diff --git a/.core_files.yaml b/.core_files.yaml index 4082c016d8f..65da342ea50 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -38,6 +38,7 @@ base_platforms: &base_platforms - homeassistant/components/siren/** - homeassistant/components/stt/** - homeassistant/components/switch/** + - homeassistant/components/text/** - homeassistant/components/tts/** - homeassistant/components/update/** - homeassistant/components/vacuum/** diff --git a/.coveragerc b/.coveragerc index 9d33acf6c75..94e1667bad8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -35,6 +35,8 @@ omit = homeassistant/components/agent_dvr/helpers.py homeassistant/components/airnow/__init__.py homeassistant/components/airnow/sensor.py + homeassistant/components/airq/__init__.py + homeassistant/components/airq/sensor.py homeassistant/components/airthings/__init__.py homeassistant/components/airthings/sensor.py homeassistant/components/airthings_ble/__init__.py @@ -232,8 +234,6 @@ omit = homeassistant/components/dlib_face_detect/image_processing.py homeassistant/components/dlib_face_identify/image_processing.py homeassistant/components/dlink/switch.py - homeassistant/components/dnsip/__init__.py - homeassistant/components/dnsip/sensor.py homeassistant/components/dominos/* homeassistant/components/doods/* homeassistant/components/doorbird/__init__.py @@ -455,7 +455,7 @@ omit = homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py - homeassistant/components/glances/__init__.py + homeassistant/components/glances/const.py homeassistant/components/glances/sensor.py homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py @@ -483,11 +483,6 @@ omit = homeassistant/components/habitica/__init__.py homeassistant/components/habitica/const.py homeassistant/components/habitica/sensor.py - homeassistant/components/hangouts/__init__.py - homeassistant/components/hangouts/hangouts_bot.py - homeassistant/components/hangouts/hangups_utils.py - homeassistant/components/hangouts/intents.py - homeassistant/components/hangouts/notify.py homeassistant/components/harman_kardon_avr/media_player.py homeassistant/components/harmony/const.py homeassistant/components/harmony/data.py @@ -676,8 +671,8 @@ omit = homeassistant/components/lcn/services.py homeassistant/components/led_ble/__init__.py homeassistant/components/led_ble/light.py - homeassistant/components/led_ble/util.py homeassistant/components/lg_netcast/media_player.py + homeassistant/components/lg_soundbar/__init__.py homeassistant/components/lg_soundbar/media_player.py homeassistant/components/lidarr/__init__.py homeassistant/components/lidarr/coordinator.py @@ -729,6 +724,9 @@ omit = homeassistant/components/map/* homeassistant/components/mastodon/notify.py homeassistant/components/matrix/* + homeassistant/components/matter/__init__.py + homeassistant/components/matter/adapter.py + homeassistant/components/matter/entity.py homeassistant/components/meater/__init__.py homeassistant/components/meater/const.py homeassistant/components/meater/sensor.py @@ -951,6 +949,8 @@ omit = homeassistant/components/overkiz/sensor.py homeassistant/components/overkiz/siren.py homeassistant/components/overkiz/switch.py + homeassistant/components/overkiz/water_heater.py + homeassistant/components/overkiz/water_heater_entities/* homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py homeassistant/components/ovo_energy/sensor.py @@ -960,6 +960,7 @@ omit = homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/diagnostics.py + homeassistant/components/philips_js/helpers.py homeassistant/components/philips_js/light.py homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/remote.py @@ -999,6 +1000,7 @@ omit = homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/pulseaudio_loopback/switch.py + homeassistant/components/pushbullet/api.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py homeassistant/components/pushover/notify.py @@ -1107,13 +1109,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/shelly/binary_sensor.py - homeassistant/components/shelly/climate.py - homeassistant/components/shelly/coordinator.py - homeassistant/components/shelly/entity.py - homeassistant/components/shelly/number.py - homeassistant/components/shelly/sensor.py - homeassistant/components/shelly/utils.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/sia/__init__.py @@ -1147,6 +1142,7 @@ omit = homeassistant/components/skybell/switch.py homeassistant/components/slack/__init__.py homeassistant/components/slack/notify.py + homeassistant/components/slack/sensor.py homeassistant/components/slide/* homeassistant/components/slimproto/__init__.py homeassistant/components/slimproto/media_player.py @@ -1574,6 +1570,7 @@ omit = homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/cover.py homeassistant/components/yolink/entity.py + homeassistant/components/yolink/light.py homeassistant/components/yolink/lock.py homeassistant/components/yolink/sensor.py homeassistant/components/yolink/siren.py diff --git a/.dockerignore b/.dockerignore index 8144367ede1..7fde7f33fa5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,7 +9,6 @@ docs .vscode # Test related files -.tox tests # Other virtualization methods diff --git a/.github/move.yml b/.github/move.yml deleted file mode 100644 index e041083c9ae..00000000000 --- a/.github/move.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Configuration for move-issues - https://github.com/dessant/move-issues - -# Delete the command comment. Ignored when the comment also contains other content -deleteCommand: true -# Close the source issue after moving -closeSourceIssue: true -# Lock the source issue after moving -lockSourceIssue: false -# Set custom aliases for targets -# aliases: -# r: repo -# or: owner/repo - diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 2f844314eaa..e4fbd33cd09 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -159,7 +159,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.09.0 + uses: home-assistant/builder@2022.11.0 with: args: | $BUILD_ARGS \ @@ -225,7 +225,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.09.0 + uses: home-assistant/builder@2022.11.0 with: args: | $BUILD_ARGS \ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fc0ca593fbd..13ff8ea12e3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 3 PIP_CACHE_VERSION: 3 - HA_SHORT_VERSION: 2022.11 + HA_SHORT_VERSION: 2022.12 DEFAULT_PYTHON: 3.9 ALL_PYTHON_VERSIONS: "['3.9', '3.10']" PRE_COMMIT_CACHE: ~/.cache/pre-commit @@ -842,7 +842,6 @@ jobs: python3 -X dev -m pytest \ -qq \ --timeout=9 \ - --durations=10 \ -n auto \ --cov="homeassistant.components.${{ matrix.group }}" \ --cov-report=xml \ @@ -936,7 +935,7 @@ jobs: . venv/bin/activate pip install mysqlclient sqlalchemy_utils - name: Run pytest (partially) - timeout-minutes: 10 + timeout-minutes: 15 shell: bash run: | . venv/bin/activate @@ -944,14 +943,13 @@ jobs: python3 -X dev -m pytest \ -qq \ - --timeout=9 \ + --timeout=20 \ -n 1 \ --cov="homeassistant.components.recorder" \ --cov-report=xml \ --cov-report=term-missing \ -o console_output_style=count \ - --durations=0 \ - --durations-min=10 \ + --durations=10 \ -p no:sugar \ --dburl=mysql://root:password@127.0.0.1/homeassistant-test \ tests/components/recorder diff --git a/.gitignore b/.gitignore index d6f7198fcd4..2e3df400c76 100644 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,6 @@ pip-log.txt # Unit test / coverage reports .coverage -.tox coverage.xml nosetests.xml htmlcov/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 751c97ebbb4..f7b1e54d0a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,16 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.2.2 hooks: - id: pyupgrade args: [--py39-plus] + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.0 + hooks: + - id: autoflake + args: + - --in-place + - --remove-all-unused-imports - repo: https://github.com/psf/black rev: 22.10.0 hooks: @@ -13,27 +20,27 @@ repos: - --quiet files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell - rev: v2.1.0 + rev: v2.2.2 hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,iif,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa,pullrequests + - --ignore-words-list=additionals,alot,ba,bre,bund,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] exclude: ^tests/fixtures/|homeassistant/generated/ - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: - - pycodestyle==2.8.0 - - pyflakes==2.4.0 + - pycodestyle==2.10.0 + - pyflakes==3.0.1 - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - - flake8-comprehensions==3.10.0 - - flake8-noqa==1.2.8 - - mccabe==0.6.1 + - flake8-comprehensions==3.10.1 + - flake8-noqa==1.3.0 + - mccabe==0.7.0 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.7.4 @@ -65,7 +72,7 @@ repos: hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.1 + rev: v2.7.1 hooks: - id: prettier - repo: https://github.com/cdce8p/python-typing-update diff --git a/.strict-typing b/.strict-typing index e79fb6b1a26..e47533d6ca9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -5,14 +5,10 @@ # Strict typing is enabled by default for core files. # Add it here to add 'disallow_any_generics'. # --- Only for core file! --- -homeassistant.exceptions -homeassistant.core -homeassistant.loader -homeassistant.requirements -homeassistant.runner -homeassistant.setup homeassistant.auth.auth_store homeassistant.auth.providers.* +homeassistant.core +homeassistant.exceptions homeassistant.helpers.area_registry homeassistant.helpers.condition homeassistant.helpers.debounce @@ -29,6 +25,10 @@ homeassistant.helpers.script_variables homeassistant.helpers.singleton homeassistant.helpers.sun homeassistant.helpers.translation +homeassistant.loader +homeassistant.requirements +homeassistant.runner +homeassistant.setup homeassistant.util.async_ homeassistant.util.color homeassistant.util.decorator @@ -79,13 +79,14 @@ homeassistant.components.button.* homeassistant.components.calendar.* homeassistant.components.camera.* homeassistant.components.canary.* -homeassistant.components.cover.* homeassistant.components.clickatell.* homeassistant.components.clicksend.* +homeassistant.components.cover.* homeassistant.components.cpuspeed.* homeassistant.components.crownstone.* homeassistant.components.deconz.* homeassistant.components.demo.* +homeassistant.components.derivative.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.devolo_home_control.* @@ -151,6 +152,7 @@ homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* homeassistant.components.ibeacon.* +homeassistant.components.image.* homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* @@ -173,17 +175,21 @@ homeassistant.components.litterrobot.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.logbook.* +homeassistant.components.logger.* homeassistant.components.lookin.* homeassistant.components.luftdaten.* homeassistant.components.mailbox.* +homeassistant.components.matter.* homeassistant.components.media_player.* homeassistant.components.media_source.* homeassistant.components.metoffice.* homeassistant.components.mikrotik.* +homeassistant.components.min_max.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* homeassistant.components.moon.* +homeassistant.components.mqtt.* homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.nanoleaf.* @@ -191,6 +197,7 @@ homeassistant.components.neato.* homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* +homeassistant.components.nextdns.* homeassistant.components.nfandroidtv.* homeassistant.components.nissan_leaf.* homeassistant.components.no_ip.* @@ -213,9 +220,9 @@ homeassistant.components.prusalink.* homeassistant.components.pure_energie.* homeassistant.components.pvoutput.* homeassistant.components.qnap_qsw.* +homeassistant.components.radarr.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* -homeassistant.components.radarr.* homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* @@ -228,12 +235,14 @@ homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.rpi_power.* homeassistant.components.rtsp_to_webrtc.* +homeassistant.components.ruuvitag_ble.* homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.schedule.* homeassistant.components.select.* homeassistant.components.senseme.* homeassistant.components.sensibo.* +homeassistant.components.sensirion_ble.* homeassistant.components.sensor.* homeassistant.components.senz.* homeassistant.components.shelly.* @@ -283,6 +292,7 @@ homeassistant.components.vacuum.* homeassistant.components.vallox.* homeassistant.components.velbus.* homeassistant.components.vlc_telnet.* +homeassistant.components.wake_on_lan.* homeassistant.components.wallbox.* homeassistant.components.water_heater.* homeassistant.components.watttime.* diff --git a/CODEOWNERS b/CODEOWNERS index acc723a3493..2966d69b032 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -45,6 +45,8 @@ build.json @home-assistant/supervisor /tests/components/airly/ @bieniu /homeassistant/components/airnow/ @asymworks /tests/components/airnow/ @asymworks +/homeassistant/components/airq/ @Sibgatulin @dl2080 +/tests/components/airq/ @Sibgatulin @dl2080 /homeassistant/components/airthings/ @danielhiversen /tests/components/airthings/ @danielhiversen /homeassistant/components/airthings_ble/ @vincegio @@ -61,8 +63,8 @@ build.json @home-assistant/supervisor /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core @frenck /tests/components/alert/ @home-assistant/core @frenck -/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy -/tests/components/alexa/ @home-assistant/cloud @ochlocracy +/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh +/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /homeassistant/components/almond/ @gcampax @balloob /tests/components/almond/ @gcampax @balloob /homeassistant/components/amberelectric/ @madpilot @@ -94,6 +96,8 @@ build.json @home-assistant/supervisor /tests/components/apprise/ @caronc /homeassistant/components/aprs/ @PhilRW /tests/components/aprs/ @PhilRW +/homeassistant/components/aranet/ @aschmitz +/tests/components/aranet/ @aschmitz /homeassistant/components/arcam_fmj/ @elupus /tests/components/arcam_fmj/ @elupus /homeassistant/components/arris_tg2492lg/ @vanbalken @@ -477,6 +481,8 @@ build.json @home-assistant/supervisor /tests/components/homeassistant/ @home-assistant/core /homeassistant/components/homeassistant_alerts/ @home-assistant/core /tests/components/homeassistant_alerts/ @home-assistant/core +/homeassistant/components/homeassistant_hardware/ @home-assistant/core +/tests/components/homeassistant_hardware/ @home-assistant/core /homeassistant/components/homeassistant_sky_connect/ @home-assistant/core /tests/components/homeassistant_sky_connect/ @home-assistant/core /homeassistant/components/homeassistant_yellow/ @home-assistant/core @@ -631,6 +637,10 @@ build.json @home-assistant/supervisor /tests/components/litejet/ @joncar /homeassistant/components/litterrobot/ @natekspencer @tkdrob /tests/components/litterrobot/ @natekspencer @tkdrob +/homeassistant/components/livisi/ @StefanIacobLivisi +/tests/components/livisi/ @StefanIacobLivisi +/homeassistant/components/local_calendar/ @allenporter +/tests/components/local_calendar/ @allenporter /homeassistant/components/local_ip/ @issacg /tests/components/local_ip/ @issacg /homeassistant/components/lock/ @home-assistant/core @@ -649,13 +659,15 @@ build.json @home-assistant/supervisor /homeassistant/components/luftdaten/ @fabaff @frenck /tests/components/luftdaten/ @fabaff @frenck /homeassistant/components/lupusec/ @majuss -/homeassistant/components/lutron/ @JonGilmore +/homeassistant/components/lutron/ @cdheiser /homeassistant/components/lutron_caseta/ @swails @bdraco @danaues /tests/components/lutron_caseta/ @swails @bdraco @danaues /homeassistant/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001 /homeassistant/components/mastodon/ @fabaff /homeassistant/components/matrix/ @tinloaf +/homeassistant/components/matter/ @MartinHjelmare @marcelveldt +/tests/components/matter/ @MartinHjelmare @marcelveldt /homeassistant/components/mazda/ @bdr99 /tests/components/mazda/ @bdr99 /homeassistant/components/meater/ @Sotolotl @emontnemery @@ -687,8 +699,8 @@ build.json @home-assistant/supervisor /tests/components/mikrotik/ @engrbm87 /homeassistant/components/mill/ @danielhiversen /tests/components/mill/ @danielhiversen -/homeassistant/components/min_max/ @fabaff -/tests/components/min_max/ @fabaff +/homeassistant/components/min_max/ @gjohansson-ST +/tests/components/min_max/ @gjohansson-ST /homeassistant/components/minecraft_server/ @elmurato /tests/components/minecraft_server/ @elmurato /homeassistant/components/minio/ @tkislan @@ -713,8 +725,8 @@ build.json @home-assistant/supervisor /tests/components/motion_blinds/ @starkillerOG /homeassistant/components/motioneye/ @dermotduffy /tests/components/motioneye/ @dermotduffy -/homeassistant/components/mqtt/ @emontnemery -/tests/components/mqtt/ @emontnemery +/homeassistant/components/mqtt/ @emontnemery @jbouwh +/tests/components/mqtt/ @emontnemery @jbouwh /homeassistant/components/msteams/ @peroyvind /homeassistant/components/mullvad/ @meichthys /tests/components/mullvad/ @meichthys @@ -776,6 +788,8 @@ build.json @home-assistant/supervisor /tests/components/nsw_fuel_station/ @nickw444 /homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte /tests/components/nsw_rural_fire_service_feed/ @exxamalte +/homeassistant/components/nuheat/ @tstabrawa +/tests/components/nuheat/ @tstabrawa /homeassistant/components/nuki/ @pschmitt @pvizeli @pree /tests/components/nuki/ @pschmitt @pvizeli @pree /homeassistant/components/numato/ @clssn @@ -878,6 +892,8 @@ build.json @home-assistant/supervisor /tests/components/pure_energie/ @klaasnicolaas /homeassistant/components/push/ @dgomes /tests/components/push/ @dgomes +/homeassistant/components/pushbullet/ @engrbm87 +/tests/components/pushbullet/ @engrbm87 /homeassistant/components/pushover/ @engrbm87 /tests/components/pushover/ @engrbm87 /homeassistant/components/pvoutput/ @frenck @@ -955,6 +971,8 @@ build.json @home-assistant/supervisor /tests/components/rtsp_to_webrtc/ @allenporter /homeassistant/components/ruckus_unleashed/ @gabe565 /tests/components/ruckus_unleashed/ @gabe565 +/homeassistant/components/ruuvitag_ble/ @akx +/tests/components/ruuvitag_ble/ @akx /homeassistant/components/sabnzbd/ @shaiu /tests/components/sabnzbd/ @shaiu /homeassistant/components/safe_mode/ @home-assistant/core @@ -985,6 +1003,8 @@ build.json @home-assistant/supervisor /tests/components/senseme/ @mikelawrence @bdraco /homeassistant/components/sensibo/ @andrey-git @gjohansson-ST /tests/components/sensibo/ @andrey-git @gjohansson-ST +/homeassistant/components/sensirion_ble/ @akx +/tests/components/sensirion_ble/ @akx /homeassistant/components/sensor/ @home-assistant/core /tests/components/sensor/ @home-assistant/core /homeassistant/components/sensorpro/ @bdraco @@ -1001,8 +1021,8 @@ build.json @home-assistant/supervisor /tests/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10 /homeassistant/components/shell_command/ @home-assistant/core /tests/components/shell_command/ @home-assistant/core -/homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74 -/tests/components/shelly/ @balloob @bieniu @thecode @chemelli74 +/homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco +/tests/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco /homeassistant/components/shodan/ @fabaff /homeassistant/components/sia/ @eavanvalkenburg /tests/components/sia/ @eavanvalkenburg @@ -1121,8 +1141,8 @@ build.json @home-assistant/supervisor /homeassistant/components/synology_srm/ @aerialls /homeassistant/components/system_bridge/ @timmo001 /tests/components/system_bridge/ @timmo001 -/homeassistant/components/tado/ @michaelarnauts @north3221 -/tests/components/tado/ @michaelarnauts @north3221 +/homeassistant/components/tado/ @michaelarnauts +/tests/components/tado/ @michaelarnauts /homeassistant/components/tag/ @balloob @dmulcahey /tests/components/tag/ @balloob @dmulcahey /homeassistant/components/tailscale/ @frenck @@ -1140,6 +1160,8 @@ build.json @home-assistant/supervisor /tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core /homeassistant/components/tesla_wall_connector/ @einarhauks /tests/components/tesla_wall_connector/ @einarhauks +/homeassistant/components/text/ @home-assistant/core +/tests/components/text/ @home-assistant/core /homeassistant/components/tfiac/ @fredrike @mellado /homeassistant/components/thermobeacon/ @bdraco /tests/components/thermobeacon/ @bdraco diff --git a/build.yaml b/build.yaml index 14a59641388..eed35c9dd69 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.10.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.10.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.10.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.10.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.10.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.11.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.11.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.11.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.11.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.11.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index cfe93a8fa1a..9dfe4f4f9ed 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -89,11 +89,21 @@ def get_arguments() -> argparse.Namespace: parser.add_argument( "--open-ui", action="store_true", help="Open the webinterface in a browser" ) - parser.add_argument( + + skip_pip_group = parser.add_mutually_exclusive_group() + skip_pip_group.add_argument( "--skip-pip", action="store_true", help="Skips pip install of required packages on startup", ) + skip_pip_group.add_argument( + "--skip-pip-packages", + metavar="package_names", + type=lambda arg: arg.split(","), + default=[], + help="Skip pip install of specific packages on startup", + ) + parser.add_argument( "-v", "--verbose", action="store_true", help="Enable verbose logging to file." ) @@ -180,6 +190,7 @@ def main() -> int: log_file=args.log_file, log_no_color=args.log_no_color, skip_pip=args.skip_pip, + skip_pip_packages=args.skip_pip_packages, safe_mode=args.safe_mode, debug=args.debug, open_ui=args.open_ui, diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 12511a7f4a5..bbd23983e2b 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -356,8 +356,7 @@ class AuthManager: provider = self._async_get_auth_provider(credentials) if provider is not None and hasattr(provider, "async_will_remove_credentials"): - # https://github.com/python/mypy/issues/1424 - await provider.async_will_remove_credentials(credentials) # type: ignore[attr-defined] + await provider.async_will_remove_credentials(credentials) await self._store.async_remove_credentials(credentials) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 61c36da6e90..ebfe1332cd2 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -166,7 +166,6 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul processed = hass.data[DATA_REQS] = set() - # https://github.com/python/mypy/issues/1424 await requirements.async_process_requirements( hass, module_path, module.REQUIREMENTS ) diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index 3f2a0c14f19..4dc221a9ff4 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -47,7 +47,7 @@ def _lookup_domain( perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str ) -> ValueType | None: """Look up entity permissions by domain.""" - return domains_dict.get(entity_id.split(".", 1)[0]) + return domains_dict.get(entity_id.partition(".")[0]) def _lookup_area( diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 6feb4b26759..2448225a284 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -250,9 +250,7 @@ class LoginFlow(data_entry_flow.FlowHandler): auth_module, "async_initialize_login_mfa_step" ): try: - await auth_module.async_initialize_login_mfa_step( # type: ignore[attr-defined] - self.user.id - ) + await auth_module.async_initialize_login_mfa_step(self.user.id) except HomeAssistantError: _LOGGER.exception("Error initializing MFA step") return self.async_abort(reason="unknown_error") diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index af9a01f5d9b..92d8d617481 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -88,12 +88,12 @@ class CommandLineAuthProvider(AuthProvider): for _line in stdout.splitlines(): try: line = _line.decode().lstrip() - if line.startswith("#"): - continue - key, value = line.split("=", 1) except ValueError: # malformed line continue + if line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") key = key.strip() value = value.strip() if key in self.ALLOWED_META_KEYS: diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 31834c7b7a3..6b91557a476 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -17,7 +17,7 @@ import voluptuous as vol import yarl from . import config as conf_util, config_entries, core, loader -from .components import http, persistent_notification +from .components import http from .const import ( REQUIRED_NEXT_PYTHON_HA_RELEASE, REQUIRED_NEXT_PYTHON_VER, @@ -53,6 +53,7 @@ ERROR_LOG_FILENAME = "home-assistant.log" # hass.data key for logging information. DATA_LOGGING = "logging" +DATA_REGISTRIES_LOADED = "bootstrap_registries_loaded" LOG_SLOW_STARTUP_INTERVAL = 60 SLOW_STARTUP_CHECK_INTERVAL = 1 @@ -117,7 +118,8 @@ async def async_setup_hass( ) hass.config.skip_pip = runtime_config.skip_pip - if runtime_config.skip_pip: + hass.config.skip_pip_packages = runtime_config.skip_pip_packages + if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( "Skipping pip installation of required modules. This may cause issues" ) @@ -175,6 +177,7 @@ async def async_setup_hass( if old_logging: hass.data[DATA_LOGGING] = old_logging hass.config.skip_pip = old_config.skip_pip + hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.internal_url = old_config.internal_url hass.config.external_url = old_config.external_url hass.config.config_dir = old_config.config_dir @@ -216,6 +219,32 @@ def open_hass_ui(hass: core.HomeAssistant) -> None: ) +async def load_registries(hass: core.HomeAssistant) -> None: + """Load the registries and cache the result of platform.uname().processor.""" + if DATA_REGISTRIES_LOADED in hass.data: + return + hass.data[DATA_REGISTRIES_LOADED] = None + + def _cache_uname_processor() -> None: + """Cache the result of platform.uname().processor in the executor. + + Multiple modules call this function at startup which + executes a blocking subprocess call. This is a problem for the + asyncio event loop. By primeing the cache of uname we can + avoid the blocking call in the event loop. + """ + platform.uname().processor # pylint: disable=expression-not-assigned + + # Load the registries and cache the result of platform.uname().processor + await asyncio.gather( + area_registry.async_load(hass), + device_registry.async_load(hass), + entity_registry.async_load(hass), + issue_registry.async_load(hass), + hass.async_add_executor_job(_cache_uname_processor), + ) + + async def async_from_config_dict( config: ConfigType, hass: core.HomeAssistant ) -> core.HomeAssistant | None: @@ -228,6 +257,7 @@ async def async_from_config_dict( hass.config_entries = config_entries.ConfigEntries(hass, config) await hass.config_entries.async_initialize() + await load_registries(hass) # Set up core. _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS) @@ -268,16 +298,31 @@ async def async_from_config_dict( REQUIRED_NEXT_PYTHON_HA_RELEASE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER ): - msg = ( - "Support for the running Python version " - f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " - f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. " - "Please upgrade Python to " - f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])}." + current_python_version = ".".join(str(x) for x in sys.version_info[:3]) + required_python_version = ".".join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2]) + _LOGGER.warning( + ( + "Support for the running Python version %s is deprecated and " + "will be removed in Home Assistant %s; " + "Please upgrade Python to %s" + ), + current_python_version, + REQUIRED_NEXT_PYTHON_HA_RELEASE, + required_python_version, ) - _LOGGER.warning(msg) - persistent_notification.async_create( - hass, msg, "Python version", "python_version" + issue_registry.async_create_issue( + hass, + core.DOMAIN, + "python_version", + is_fixable=False, + severity=issue_registry.IssueSeverity.WARNING, + breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE, + translation_key="python_version", + translation_placeholders={ + "current_python_version": current_python_version, + "required_python_version": required_python_version, + "breaks_in_ha_version": REQUIRED_NEXT_PYTHON_HA_RELEASE, + }, ) return hass @@ -404,7 +449,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str: def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: """Get domains of components to set up.""" # Filter out the repeating and common config section [homeassistant] - domains = {key.split(" ")[0] for key in config if key != core.DOMAIN} + domains = {key.partition(" ")[0] for key in config if key != core.DOMAIN} # Add config entry domains if not hass.config.safe_mode: @@ -515,25 +560,6 @@ async def _async_set_up_integrations( _LOGGER.info("Domains to be set up: %s", domains_to_setup) - def _cache_uname_processor() -> None: - """Cache the result of platform.uname().processor in the executor. - - Multiple modules call this function at startup which - executes a blocking subprocess call. This is a problem for the - asyncio event loop. By primeing the cache of uname we can - avoid the blocking call in the event loop. - """ - platform.uname().processor # pylint: disable=expression-not-assigned - - # Load the registries and cache the result of platform.uname().processor - await asyncio.gather( - area_registry.async_load(hass), - device_registry.async_load(hass), - entity_registry.async_load(hass), - issue_registry.async_load(hass), - hass.async_add_executor_job(_cache_uname_processor), - ) - # Initialize recorder if "recorder" in domains_to_setup: recorder.async_initialize_recorder(hass) diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index 5f37de46180..de27fa7c515 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -14,7 +14,6 @@ "google", "nest", "cast", - "hangouts", "dialogflow" ] } diff --git a/homeassistant/brands/yamaha.json b/homeassistant/brands/yamaha.json new file mode 100644 index 00000000000..d25e85f1b25 --- /dev/null +++ b/homeassistant/brands/yamaha.json @@ -0,0 +1,5 @@ +{ + "domain": "yamaha", + "name": "Yamaha", + "integrations": ["yamaha", "yamaha_musiccast"] +} diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 4f7af8af640..08ed1925936 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -1,4 +1,7 @@ """Support for Abode Security System binary sensors.""" +from __future__ import annotations + +from contextlib import suppress from typing import cast from abodepy.devices.binary_sensor import AbodeBinarySensor as ABBinarySensor @@ -47,8 +50,10 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): return cast(bool, self._device.is_on) @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of the binary sensor.""" if self._device.get_value("is_window") == "1": return BinarySensorDeviceClass.WINDOW - return cast(str, self._device.generic_type) + with suppress(ValueError): + return BinarySensorDeviceClass(cast(str, self._device.generic_type)) + return None diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 030e5744ce8..b930c3d654b 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -116,8 +116,3 @@ class AbodeLight(AbodeDevice, LightEntity): if self._device.is_dimmable: return {ColorMode.BRIGHTNESS} return {ColorMode.ONOFF} - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return 0 diff --git a/homeassistant/components/abode/translations/bg.json b/homeassistant/components/abode/translations/bg.json index a451dd3516a..6a58c61203d 100644 --- a/homeassistant/components/abode/translations/bg.json +++ b/homeassistant/components/abode/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode." }, "error": { diff --git a/homeassistant/components/abode/translations/sk.json b/homeassistant/components/abode/translations/sk.json index 2230fa979b4..1d4e1148fb0 100644 --- a/homeassistant/components/abode/translations/sk.json +++ b/homeassistant/components/abode/translations/sk.json @@ -1,21 +1,34 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_mfa_code": "Neplatn\u00fd k\u00f3d MFA" }, "step": { + "mfa": { + "data": { + "mfa_code": "K\u00f3d MFA (6-miestny)" + }, + "title": "Zadajte svoj k\u00f3d MFA pre Abode" + }, "reauth_confirm": { "data": { + "password": "Heslo", "username": "Email" - } + }, + "title": "Vypl\u0148te svoje prihlasovacie \u00fadaje do slu\u017eby Abode" }, "user": { "data": { + "password": "Heslo", "username": "Email" - } + }, + "title": "Vypl\u0148te svoje prihlasovacie \u00fadaje Abode" } } } diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index b9244a3645c..1480f6c1352 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -17,9 +17,22 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import CONF_FORECAST, DOMAIN +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_FORECAST, default=False): bool, + } +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for AccuWeather.""" @@ -84,41 +97,6 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> AccuWeatherOptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: """Options callback for AccuWeather.""" - return AccuWeatherOptionsFlowHandler(config_entry) - - -class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow): - """Config flow options for AccuWeather.""" - - def __init__(self, entry: ConfigEntry) -> None: - """Initialize AccuWeather options flow.""" - self.config_entry = entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the options.""" - return await self.async_step_user() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Optional( - CONF_FORECAST, - default=self.config_entry.options.get(CONF_FORECAST, False), - ): bool - } - ), - ) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 78041c5309c..2dbefe19965 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -253,7 +253,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( name="Cloud ceiling", state_class=SensorStateClass.MEASUREMENT, unit_fn=lambda metric: LENGTH_METERS if metric else LENGTH_FEET, - value_fn=lambda data, unit: round(data[unit][ATTR_VALUE]), + value_fn=lambda data, unit: round(cast(float, data[unit][ATTR_VALUE])), ), AccuWeatherSensorDescription( key="CloudCover", diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 432cc095c7b..ba1bba21d9e 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -24,7 +24,7 @@ }, "options": { "step": { - "user": { + "init": { "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/bg.json b/homeassistant/components/accuweather/translations/bg.json index 8435bea00df..9c5e3075643 100644 --- a/homeassistant/components/accuweather/translations/bg.json +++ b/homeassistant/components/accuweather/translations/bg.json @@ -20,6 +20,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e" + }, + "description": "\u041f\u043e\u0440\u0430\u0434\u0438 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u0442\u0430 \u043d\u0430 \u0431\u0435\u0437\u043f\u043b\u0430\u0442\u043d\u0430\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 API \u043a\u043b\u044e\u0447\u0430 \u043d\u0430 AccuWeather, \u043a\u043e\u0433\u0430\u0442\u043e \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430\u0442\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438\u0442\u0435 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438 \u0449\u0435 \u0441\u0435 \u0438\u0437\u0432\u044a\u0440\u0448\u0432\u0430\u0442 \u043d\u0430 \u0432\u0441\u0435\u043a\u0438 80 \u043c\u0438\u043d\u0443\u0442\u0438 \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0430 \u0432\u0441\u0435\u043a\u0438 40 \u043c\u0438\u043d\u0443\u0442\u0438." + }, "user": { "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e" diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index e80a006141a..54b93643e8e 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -24,6 +24,11 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Previsi\u00f3 meteorol\u00f2gica" + } + }, "user": { "data": { "forecast": "Previsi\u00f3 meteorol\u00f2gica" diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json index e3ae982cddc..f0796ff4d1e 100644 --- a/homeassistant/components/accuweather/translations/cs.json +++ b/homeassistant/components/accuweather/translations/cs.json @@ -21,6 +21,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed" + }, + "description": "Vzhledem k omezen\u00edm bezplatn\u00e9 verze kl\u00ed\u010de AccuWeather API, kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, aktualizace dat se budou prov\u00e1d\u011bt ka\u017ed\u00fdch 80 minut m\u00edsto ka\u017ed\u00fdch 40 minut." + }, "user": { "data": { "forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed" diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index f7b02feb091..6a3c887d881 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Wettervorhersage" + }, + "description": "Aufgrund der Einschr\u00e4nkungen der kostenlosen Version des AccuWeather API-Schl\u00fcssels werden bei aktivierter Wettervorhersage Datenaktualisierungen alle 80 Minuten statt alle 40 Minuten durchgef\u00fchrt." + }, "user": { "data": { "forecast": "Wettervorhersage" diff --git a/homeassistant/components/accuweather/translations/el.json b/homeassistant/components/accuweather/translations/el.json index b8d7d22df70..a0eca3e75fc 100644 --- a/homeassistant/components/accuweather/translations/el.json +++ b/homeassistant/components/accuweather/translations/el.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "\u03a0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd" + }, + "description": "\u039b\u03cc\u03b3\u03c9 \u03c4\u03c9\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ce\u03bd \u03c4\u03b7\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd AccuWeather API, \u03cc\u03c4\u03b1\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd, \u03bf\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b8\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 80 \u03bb\u03b5\u03c0\u03c4\u03ac \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 40 \u03bb\u03b5\u03c0\u03c4\u03ac." + }, "user": { "data": { "forecast": "\u03a0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd" diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json index d391ce83e1e..844970f0d2b 100644 --- a/homeassistant/components/accuweather/translations/en.json +++ b/homeassistant/components/accuweather/translations/en.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "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 80 minutes instead of every 40 minutes." + }, "user": { "data": { "forecast": "Weather forecast" diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index 9ec67fd88b8..094d725c2f4 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { - "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puedes habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n." + "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puedes habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\nLa previsi\u00f3n meteorol\u00f3gica no est\u00e1 habilitada de forma predeterminada. Puedes habilitarla en las opciones de integraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", @@ -24,11 +24,17 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Previsi\u00f3n meteorol\u00f3gica" + }, + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas la previsi\u00f3n meteorol\u00f3gica, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos." + }, "user": { "data": { - "forecast": "Pron\u00f3stico del tiempo" + "forecast": "Previsi\u00f3n meteorol\u00f3gica" }, - "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos." + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas la previsi\u00f3n meteorol\u00f3gica, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos." } } }, diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index 2f85bd8663e..9ce64fdd91c 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Ilmateade" + }, + "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit)." + }, "user": { "data": { "forecast": "Ilmateade" diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 75ef209a1cf..187b1fb3e0b 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -24,6 +24,11 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques" + } + }, "user": { "data": { "forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques" diff --git a/homeassistant/components/accuweather/translations/id.json b/homeassistant/components/accuweather/translations/id.json index 1cd49752342..72a4fab86f8 100644 --- a/homeassistant/components/accuweather/translations/id.json +++ b/homeassistant/components/accuweather/translations/id.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Prakiraan cuaca" + }, + "description": "Karena keterbatasan versi gratis kunci API AccuWeather, ketika Anda mengaktifkan prakiraan cuaca, pembaruan data akan dilakukan setiap 80 menit, bukan setiap 40 menit." + }, "user": { "data": { "forecast": "Prakiraan cuaca" diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index b9c7819e78a..3583f9760b9 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -28,7 +28,7 @@ "data": { "forecast": "\u5929\u6c17\u4e88\u5831" }, - "description": "\u5236\u9650\u306b\u3088\u308a\u7121\u6599\u30d0\u30fc\u30b8\u30e7\u30f3\u306eAccuWeather API\u30ad\u30fc\u3067\u306f\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u3067\u306f\u306a\u304f80\u5206\u3054\u3068\u3067\u3059\u3002" + "description": "\u7121\u6599\u7248\u306eAccuWeather API\u30ad\u30fc\u306e\u5236\u9650\u306b\u3088\u308a\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u305f\u5834\u5408\u3001\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u6bce\u3067\u306f\u306a\u304f80\u5206\u6bce\u306b\u5b9f\u884c\u3055\u308c\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/accuweather/translations/nl.json b/homeassistant/components/accuweather/translations/nl.json index df5c71dcf45..9b18af3b6da 100644 --- a/homeassistant/components/accuweather/translations/nl.json +++ b/homeassistant/components/accuweather/translations/nl.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Weersverwachting" + }, + "description": "Wanneer je de weersverwachting ingeschakeld zullen updates elke 80 minuten plaatsvinden i.p.v. elke 40 minuten, dit komt door de beperkingen van de gratis versie van de AccuWeather API sleutel." + }, "user": { "data": { "forecast": "Weervoorspelling" diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index dc203abe714..c6af3b8320c 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "V\u00e6rmelding" + }, + "description": "P\u00e5 grunn av begrensningene til gratisversjonen av AccuWeather API-n\u00f8kkelen, n\u00e5r du aktiverer v\u00e6rmelding, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt." + }, "user": { "data": { "forecast": "V\u00e6rmelding" diff --git a/homeassistant/components/accuweather/translations/pt-BR.json b/homeassistant/components/accuweather/translations/pt-BR.json index 3fc0d95ad2a..726de96c37d 100644 --- a/homeassistant/components/accuweather/translations/pt-BR.json +++ b/homeassistant/components/accuweather/translations/pt-BR.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "Previs\u00e3o do tempo" + }, + "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave API AccuWeather, quando voc\u00ea ativa a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados s\u00e3o realizadas a cada 80 minutos em vez de 40 minutos." + }, "user": { "data": { "forecast": "Previs\u00e3o do Tempo" diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 2f08263b4f5..8f137f71c74 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "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 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442." + }, "user": { "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b" diff --git a/homeassistant/components/accuweather/translations/sensor.sk.json b/homeassistant/components/accuweather/translations/sensor.sk.json new file mode 100644 index 00000000000..4152daff605 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.sk.json @@ -0,0 +1,8 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Klesaj\u00faci", + "rising": "Padaj\u00faci" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sk.json b/homeassistant/components/accuweather/translations/sk.json index 8e0bc629a13..d39416fdab7 100644 --- a/homeassistant/components/accuweather/translations/sk.json +++ b/homeassistant/components/accuweather/translations/sk.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { - "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "requests_exceeded": "Povolen\u00fd po\u010det po\u017eiadaviek na rozhranie Accuweather API bol prekro\u010den\u00fd. Mus\u00edte po\u010dka\u0165 alebo zmeni\u0165 k\u013e\u00fa\u010d API." }, "step": { "user": { @@ -13,5 +18,25 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "forecast": "Predpove\u010f po\u010dasia" + } + }, + "user": { + "data": { + "forecast": "Predpove\u010f po\u010dasia" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Oslovi\u0165 server AccuWeather", + "remaining_requests": "Zost\u00e1vaj\u00face povolen\u00e9 \u017eiadosti" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/zh-Hans.json b/homeassistant/components/accuweather/translations/zh-Hans.json index f8879f5715f..248c9ff8e55 100644 --- a/homeassistant/components/accuweather/translations/zh-Hans.json +++ b/homeassistant/components/accuweather/translations/zh-Hans.json @@ -1,4 +1,14 @@ { + "options": { + "step": { + "init": { + "data": { + "forecast": "\u5929\u6c14\u9884\u62a5" + }, + "description": "\u7531\u4e8e AccuWeather API \u5bc6\u94a5\u514d\u8d39\u7248\u672c\u7684\u9650\u5236\uff0c\u5f53\u60a8\u542f\u7528\u5929\u6c14\u9884\u62a5\u65f6\uff0c\u6570\u636e\u66f4\u65b0\u5c06\u6bcf 80 \u5206\u949f\u6267\u884c\u4e00\u6b21\uff0c\u800c\u4e0d\u662f\u6bcf 40 \u5206\u949f\u4e00\u6b21\u3002" + } + } + }, "system_health": { "info": { "can_reach_server": "\u53ef\u8bbf\u95ee AccuWeather \u670d\u52a1\u5668", diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index c7476b33460..03bfb362e30 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -24,6 +24,12 @@ }, "options": { "step": { + "init": { + "data": { + "forecast": "\u5929\u6c23\u9810\u5831" + }, + "description": "\u7531\u65bc AccuWeather API \u91d1\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" + }, "user": { "data": { "forecast": "\u5929\u6c23\u9810\u5831" diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 82db25288b8..5c5ba303ad5 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -18,16 +18,11 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - PRESSURE_INHG, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -72,19 +67,19 @@ class AccuWeatherEntity( # converted, hence the weather entity's native units follow the configured unit # system if coordinator.hass.config.units is METRIC_SYSTEM: - self._attr_native_precipitation_unit = LENGTH_MILLIMETERS - self._attr_native_pressure_unit = PRESSURE_HPA - self._attr_native_temperature_unit = TEMP_CELSIUS - self._attr_native_visibility_unit = LENGTH_KILOMETERS - self._attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + self._attr_native_pressure_unit = UnitOfPressure.HPA + self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_native_visibility_unit = UnitOfLength.KILOMETERS + self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR self._unit_system = API_METRIC else: self._unit_system = API_IMPERIAL - self._attr_native_precipitation_unit = LENGTH_INCHES - self._attr_native_pressure_unit = PRESSURE_INHG - self._attr_native_temperature_unit = TEMP_FAHRENHEIT - self._attr_native_visibility_unit = LENGTH_MILES - self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR + self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.INCHES + self._attr_native_pressure_unit = UnitOfPressure.INHG + self._attr_native_temperature_unit = UnitOfTemperature.FAHRENHEIT + self._attr_native_visibility_unit = UnitOfLength.MILES + self._attr_native_wind_speed_unit = UnitOfSpeed.MILES_PER_HOUR self._attr_unique_id = coordinator.location_key self._attr_attribution = ATTRIBUTION self._attr_device_info = coordinator.device_info diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index 1db629e506a..f1bd0613f1e 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from contextlib import suppress +from typing import Any import aiopulse import async_timeout @@ -10,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_ID +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -19,11 +21,13 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" self.discovered_hubs: dict[str, aiopulse.Hub] | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if ( user_input is not None @@ -37,7 +41,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries() } - hubs = [] + hubs: list[aiopulse.Hub] = [] with suppress(asyncio.TimeoutError): async with async_timeout.timeout(5): async for hub in aiopulse.Hub.discover(): @@ -63,7 +67,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_create(self, hub): + async def async_create(self, hub: aiopulse.Hub) -> FlowResult: """Create the Acmeda Hub entry.""" await self.async_set_unique_id(hub.id, raise_on_progress=False) return self.async_create_entry(title=hub.id, data={CONF_HOST: hub.host}) diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index 3c3a5ba825a..15a20cf6932 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -70,9 +70,9 @@ class AcmedaCover(AcmedaBase, CoverEntity): return position @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" - supported_features = 0 + supported_features = CoverEntityFeature(0) if self.current_cover_position is not None: supported_features |= ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/acmeda/translations/he.json b/homeassistant/components/acmeda/translations/he.json index 498f322a7b0..15b5629aa52 100644 --- a/homeassistant/components/acmeda/translations/he.json +++ b/homeassistant/components/acmeda/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "step": { "user": { diff --git a/homeassistant/components/acmeda/translations/sk.json b/homeassistant/components/acmeda/translations/sk.json new file mode 100644 index 00000000000..f1ebfe35ab4 --- /dev/null +++ b/homeassistant/components/acmeda/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "step": { + "user": { + "data": { + "id": "ID hostite\u013ea" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/sk.json b/homeassistant/components/adax/translations/sk.json index af9d83e3703..5232969e1a8 100644 --- a/homeassistant/components/adax/translations/sk.json +++ b/homeassistant/components/adax/translations/sk.json @@ -1,7 +1,33 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "heater_not_available": "Ohrieva\u010d nie je k dispoz\u00edcii. Sk\u00faste resetova\u0165 ohrieva\u010d stla\u010den\u00edm + a OK na nieko\u013eko sek\u00fand.", + "heater_not_found": "Ohrieva\u010d sa nena\u0161iel. Sk\u00faste presun\u00fa\u0165 ohrieva\u010d bli\u017e\u0161ie k Home Assistant.", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "cloud": { + "data": { + "account_id": "ID \u00fa\u010dtu", + "password": "Heslo" + } + }, + "local": { + "data": { + "wifi_pswd": "Heslo Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + } + }, + "user": { + "data": { + "connection_type": "Vyberte typ pripojenia" + }, + "description": "Vyberte typ pripojenia. Miestne vy\u017eaduje ohrieva\u010de s bluetooth" + } } } } \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/sk.json b/homeassistant/components/adguard/translations/sk.json index 892b8b2cd91..10e7c5b3860 100644 --- a/homeassistant/components/adguard/translations/sk.json +++ b/homeassistant/components/adguard/translations/sk.json @@ -1,9 +1,25 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "existing_instance_updated": "Aktualizovan\u00e1 existuj\u00faca konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurova\u0165 Home Assistant na pripojenie k AdGuard Home poskytovan\u00e9mu doplnkom: {addon}?", + "title": "AdGuard Home cez doplnok Home Assistant" + }, "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } } diff --git a/homeassistant/components/advantage_air/translations/sk.json b/homeassistant/components/advantage_air/translations/sk.json index 892b8b2cd91..7abc08b46d9 100644 --- a/homeassistant/components/advantage_air/translations/sk.json +++ b/homeassistant/components/advantage_air/translations/sk.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { + "ip_address": "IP adresa", "port": "Port" - } + }, + "title": "Pripoji\u0165" } } } diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index 1188df5b94f..9db0c6f7db1 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -8,9 +8,22 @@ from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_STATION_UPDATES): bool, + } +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for AEMET OpenData.""" @@ -54,32 +67,9 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: + ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for AEMET.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_STATION_UPDATES, - default=self.config_entry.options.get(CONF_STATION_UPDATES), - ): bool, - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def _is_aemet_api_online(hass, api_key): diff --git a/homeassistant/components/aemet/translations/sk.json b/homeassistant/components/aemet/translations/sk.json index 3c287c2d9d2..dffe47482f1 100644 --- a/homeassistant/components/aemet/translations/sk.json +++ b/homeassistant/components/aemet/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, "error": { "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" }, @@ -8,7 +11,18 @@ "data": { "api_key": "API k\u013e\u00fa\u010d", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov integr\u00e1cie" + }, + "description": "Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://opendata.aemet.es/centrodedescargas/altaUsuario" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Zhroma\u017edi\u0165 \u00fadaje z meteorologick\u00fdch stan\u00edc AEMET" } } } diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 695ae283a47..3753d8e33ff 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -12,10 +12,10 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -91,10 +91,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__( self, diff --git a/homeassistant/components/agent_dvr/translations/sk.json b/homeassistant/components/agent_dvr/translations/sk.json index ba2680ac75e..b6699663a03 100644 --- a/homeassistant/components/agent_dvr/translations/sk.json +++ b/homeassistant/components/agent_dvr/translations/sk.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165" }, "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/airly/translations/cs.json b/homeassistant/components/airly/translations/cs.json index 3b4f98f53f8..766ca31099f 100644 --- a/homeassistant/components/airly/translations/cs.json +++ b/homeassistant/components/airly/translations/cs.json @@ -21,7 +21,8 @@ }, "system_health": { "info": { - "can_reach_server": "Lze kontaktovat Airly server" + "can_reach_server": "Lze kontaktovat Airly server", + "requests_per_day": "Povolen\u00e9 po\u017eadavky za den" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sk.json b/homeassistant/components/airly/translations/sk.json index 8e0bc629a13..0c8474a0725 100644 --- a/homeassistant/components/airly/translations/sk.json +++ b/homeassistant/components/airly/translations/sk.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "wrong_location": "V tejto oblasti nie s\u00fa \u017eiadne meracie stanice Airly." }, "step": { "user": { @@ -10,8 +14,16 @@ "latitude": "Zemepisn\u00e1 \u0161\u00edrka", "longitude": "Zemepisn\u00e1 d\u013a\u017eka", "name": "N\u00e1zov" - } + }, + "description": "Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://developer.airly.eu/register" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dosta\u0148te sa na server Airly", + "requests_per_day": "Povolen\u00e9 po\u017eiadavky za de\u0148", + "requests_remaining": "Zost\u00e1vaj\u00face povolen\u00e9 \u017eiadosti" + } } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/sk.json b/homeassistant/components/airnow/translations/sk.json index df686b2a565..630e30719bf 100644 --- a/homeassistant/components/airnow/translations/sk.json +++ b/homeassistant/components/airnow/translations/sk.json @@ -1,15 +1,23 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_location": "Pre t\u00fato lokalitu sa nena\u0161li \u017eiadne v\u00fdsledky", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" - } + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "radius": "Polomer stanice (m\u00edle; volite\u013en\u00e9)" + }, + "description": "Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airq/__init__.py b/homeassistant/components/airq/__init__.py new file mode 100644 index 00000000000..4bc64e1e825 --- /dev/null +++ b/homeassistant/components/airq/__init__.py @@ -0,0 +1,78 @@ +"""The air-Q integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioairq import AirQ + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +class AirQCoordinator(DataUpdateCoordinator): + """Coordinator is responsible for querying the device at a specified route.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + ) -> None: + """Initialise a custom coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + session = async_get_clientsession(hass) + self.airq = AirQ( + entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], session + ) + self.device_id = entry.unique_id + assert self.device_id is not None + self.device_info = DeviceInfo( + manufacturer=MANUFACTURER, + identifiers={(DOMAIN, self.device_id)}, + ) + self.device_info.update(entry.data["device_info"]) + + async def _async_update_data(self) -> dict: + """Fetch the data from the device.""" + data = await self.airq.get(TARGET_ROUTE) + return self.airq.drop_uncertainties_from_data(data) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up air-Q from a config entry.""" + + coordinator = AirQCoordinator(hass, entry) + + # Query the device for the first time and initialise coordinator.data + await coordinator.async_config_entry_first_refresh() + + # Record the coordinator in a global store + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/airq/config_flow.py b/homeassistant/components/airq/config_flow.py new file mode 100644 index 00000000000..05af6825233 --- /dev/null +++ b/homeassistant/components/airq/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for air-Q integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aioairq import AirQ, InvalidAuth, InvalidInput +from aiohttp.client_exceptions import ClientConnectionError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for air-Q.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial (authentication) configuration step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors: dict[str, str] = {} + + session = async_get_clientsession(self.hass) + try: + airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session) + except InvalidInput: + _LOGGER.debug( + "%s does not appear to be a valid IP address or mDNS name", + user_input[CONF_IP_ADDRESS], + ) + errors["base"] = "invalid_input" + else: + try: + await airq.validate() + except ClientConnectionError: + _LOGGER.debug( + "Failed to connect to device %s. Check the IP address / device ID " + "as well as whether the device is connected to power and the WiFi", + user_input[CONF_IP_ADDRESS], + ) + errors["base"] = "cannot_connect" + except InvalidAuth: + _LOGGER.debug( + "Incorrect password for device %s", user_input[CONF_IP_ADDRESS] + ) + errors["base"] = "invalid_auth" + else: + _LOGGER.debug( + "Successfully connected to %s", user_input[CONF_IP_ADDRESS] + ) + + device_info = await airq.fetch_device_info() + await self.async_set_unique_id(device_info.pop("id")) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=device_info["name"], + data=user_input | {"device_info": device_info}, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/airq/const.py b/homeassistant/components/airq/const.py new file mode 100644 index 00000000000..82719515cbf --- /dev/null +++ b/homeassistant/components/airq/const.py @@ -0,0 +1,9 @@ +"""Constants for the air-Q integration.""" +from typing import Final + +DOMAIN: Final = "airq" +MANUFACTURER: Final = "CorantGmbH" +TARGET_ROUTE: Final = "average" +CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³" +ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³" +UPDATE_INTERVAL: float = 10.0 diff --git a/homeassistant/components/airq/manifest.json b/homeassistant/components/airq/manifest.json new file mode 100644 index 00000000000..932b404278d --- /dev/null +++ b/homeassistant/components/airq/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "airq", + "name": "air-Q", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airq", + "requirements": ["aioairq==0.2.4"], + "codeowners": ["@Sibgatulin", "@dl2080"], + "iot_class": "local_polling", + "loggers": ["aioairq"], + "integration_type": "hub" +} diff --git a/homeassistant/components/airq/sensor.py b/homeassistant/components/airq/sensor.py new file mode 100644 index 00000000000..c524050ea66 --- /dev/null +++ b/homeassistant/components/airq/sensor.py @@ -0,0 +1,361 @@ +"""Definition of air-Q sensor platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Literal + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + PRESSURE_HPA, + SOUND_PRESSURE_WEIGHTED_DBA, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import AirQCoordinator +from .const import ( + ACTIVITY_BECQUEREL_PER_CUBIC_METER, + CONCENTRATION_GRAMS_PER_CUBIC_METER, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class AirQEntityDescriptionMixin: + """Class for keys required by AirQ entity.""" + + value: Callable[[dict], float | int | None] + + +@dataclass +class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin): + """Describes AirQ sensor entity.""" + + +# Keys must match those in the data dictionary +SENSOR_TYPES: list[AirQEntityDescription] = [ + AirQEntityDescription( + key="nh3_MR100", + name="Ammonia", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("nh3_MR100"), + ), + AirQEntityDescription( + key="cl2_M20", + name="Chlorine", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("cl2_M20"), + ), + AirQEntityDescription( + key="co", + name="CO", + device_class=SensorDeviceClass.CO, + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("co"), + ), + AirQEntityDescription( + key="co2", + name="CO2", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("co2"), + ), + AirQEntityDescription( + key="dewpt", + name="Dew point", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("dewpt"), + icon="mdi:water-thermometer", + ), + AirQEntityDescription( + key="ethanol", + name="Ethanol", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("ethanol"), + ), + AirQEntityDescription( + key="ch2o_M10", + name="Formaldehyde", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("ch2o_M10"), + ), + AirQEntityDescription( + key="h2s", + name="H2S", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("h2s"), + ), + AirQEntityDescription( + key="health", + name="Health Index", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:heart-pulse", + value=lambda data: data.get("health", 0.0) / 10.0, + ), + AirQEntityDescription( + key="humidity", + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("humidity"), + ), + AirQEntityDescription( + key="humidity_abs", + name="Absolute humidity", + native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("humidity_abs"), + icon="mdi:water", + ), + AirQEntityDescription( + key="h2_M1000", + name="Hydrogen", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("h2_M1000"), + ), + AirQEntityDescription( + key="ch4_MIPEX", + name="Methane", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("ch4_MIPEX"), + ), + AirQEntityDescription( + key="n2o", + name="N2O", + device_class=SensorDeviceClass.NITROUS_OXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("n2o"), + ), + AirQEntityDescription( + key="no_M250", + name="NO", + device_class=SensorDeviceClass.NITROGEN_MONOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("no_M250"), + ), + AirQEntityDescription( + key="no2", + name="NO2", + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("no2"), + ), + AirQEntityDescription( + key="o3", + name="Ozone", + device_class=SensorDeviceClass.OZONE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("o3"), + ), + AirQEntityDescription( + key="oxygen", + name="Oxygen", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("oxygen"), + icon="mdi:leaf", + ), + AirQEntityDescription( + key="performance", + name="Performance Index", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:head-check", + value=lambda data: data.get("performance", 0.0) / 10.0, + ), + AirQEntityDescription( + key="pm1", + name="PM1", + device_class=SensorDeviceClass.PM1, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pm1"), + icon="mdi:dots-hexagon", + ), + AirQEntityDescription( + key="pm2_5", + name="PM2.5", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pm2_5"), + icon="mdi:dots-hexagon", + ), + AirQEntityDescription( + key="pm10", + name="PM10", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pm10"), + icon="mdi:dots-hexagon", + ), + AirQEntityDescription( + key="pressure", + name="Pressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pressure"), + ), + AirQEntityDescription( + key="pressure_rel", + name="Relative pressure", + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pressure_rel"), + icon="mdi:gauge", + ), + AirQEntityDescription( + key="c3h8_MIPEX", + name="Propane", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("c3h8_MIPEX"), + ), + AirQEntityDescription( + key="so2", + name="SO2", + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("so2"), + ), + AirQEntityDescription( + key="sound", + name="Noise", + native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("sound"), + icon="mdi:ear-hearing", + ), + AirQEntityDescription( + key="sound_max", + name="Noise (Maximum)", + native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("sound_max"), + icon="mdi:ear-hearing", + ), + AirQEntityDescription( + key="radon", + name="Radon", + native_unit_of_measurement=ACTIVITY_BECQUEREL_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("radon"), + icon="mdi:radioactive", + ), + AirQEntityDescription( + key="temperature", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("temperature"), + ), + AirQEntityDescription( + key="tvoc", + name="VOC", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("tvoc"), + ), + AirQEntityDescription( + key="tvoc_ionsc", + name="VOC (Industrial)", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("tvoc_ionsc"), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensor entities based on a config entry.""" + + coordinator = hass.data[DOMAIN][config.entry_id] + + entities: list[AirQSensor] = [] + + device_status: dict[str, str] | Literal["OK"] = coordinator.data["Status"] + + for description in SENSOR_TYPES: + if description.key not in coordinator.data: + if isinstance( + device_status, dict + ) and "sensor still in warm up phase" in device_status.get( + description.key, "OK" + ): + # warming up sensors do not contribute keys to coordinator.data + # but still must be added + _LOGGER.debug("Following sensor is warming up: %s", description.key) + else: + continue + entities.append(AirQSensor(coordinator, description)) + + async_add_entities(entities) + + +class AirQSensor(CoordinatorEntity, SensorEntity): + """Representation of a Sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AirQCoordinator, + description: AirQEntityDescription, + ) -> None: + """Initialize a single sensor.""" + super().__init__(coordinator) + self.entity_description: AirQEntityDescription = description + + self._attr_device_info = coordinator.device_info + self._attr_name = description.name + self._attr_unique_id = f"{coordinator.device_id}_{description.key}" + self._attr_native_value = description.value(coordinator.data) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = self.entity_description.value(self.coordinator.data) + self.async_write_ha_state() diff --git a/homeassistant/components/airq/strings.json b/homeassistant/components/airq/strings.json new file mode 100644 index 00000000000..3618d9d517e --- /dev/null +++ b/homeassistant/components/airq/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "title": "Identify the device", + "description": "Provide the IP address or mDNS of the device and its password", + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_input": "[%key:common::config_flow::error::invalid_host%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/airq/translations/bg.json b/homeassistant/components/airq/translations/bg.json new file mode 100644 index 00000000000..df43ab876ba --- /dev/null +++ b/homeassistant/components/airq/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_input": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u0438\u043b\u0438 mDNS \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438 \u043d\u0435\u0433\u043e\u0432\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", + "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/ca.json b/homeassistant/components/airq/translations/ca.json new file mode 100644 index 00000000000..b9784605a9b --- /dev/null +++ b/homeassistant/components/airq/translations/ca.json @@ -0,0 +1,22 @@ +{ + "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_input": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP", + "password": "Contrasenya" + }, + "description": "Proporciona l'adre\u00e7a IP o el mDNS del dispositiu i la seva contrasenya", + "title": "Identificaci\u00f3 del dispositiu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/cs.json b/homeassistant/components/airq/translations/cs.json new file mode 100644 index 00000000000..efbce0227ab --- /dev/null +++ b/homeassistant/components/airq/translations/cs.json @@ -0,0 +1,20 @@ +{ + "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", + "invalid_input": "Neplatn\u00fd hostitel nebo IP adresa" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/de.json b/homeassistant/components/airq/translations/de.json new file mode 100644 index 00000000000..bccfc24be6c --- /dev/null +++ b/homeassistant/components/airq/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_input": "Ung\u00fcltiger Hostname oder IP-Adresse" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse", + "password": "Passwort" + }, + "description": "Gib die IP-Adresse oder den mDNS des Ger\u00e4ts und sein Passwort an", + "title": "Identifizieren des Ger\u00e4ts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/el.json b/homeassistant/components/airq/translations/el.json new file mode 100644 index 00000000000..594e0aaf188 --- /dev/null +++ b/homeassistant/components/airq/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "invalid_input": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf mDNS \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c4\u03b7\u03c2", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/en.json b/homeassistant/components/airq/translations/en.json new file mode 100644 index 00000000000..c8ae857d10d --- /dev/null +++ b/homeassistant/components/airq/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_input": "Invalid hostname or IP address" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address", + "password": "Password" + }, + "description": "Provide the IP address or mDNS of the device and its password", + "title": "Identify the device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/es.json b/homeassistant/components/airq/translations/es.json new file mode 100644 index 00000000000..e3a543daecb --- /dev/null +++ b/homeassistant/components/airq/translations/es.json @@ -0,0 +1,22 @@ +{ + "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_input": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a" + }, + "description": "Proporciona la direcci\u00f3n IP o mDNS del dispositivo y su contrase\u00f1a", + "title": "Identificar el dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/et.json b/homeassistant/components/airq/translations/et.json new file mode 100644 index 00000000000..7045d3d5278 --- /dev/null +++ b/homeassistant/components/airq/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_input": "Hostinimi v\u00f5i IP aadress vigane" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress", + "password": "Salas\u00f5na" + }, + "description": "Sisesta seadme IP-aadress v\u00f5i mDNS ja parool", + "title": "Seadme tuvastamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/fr.json b/homeassistant/components/airq/translations/fr.json new file mode 100644 index 00000000000..cbee413a351 --- /dev/null +++ b/homeassistant/components/airq/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "invalid_input": "Nom d'h\u00f4te ou adresse IP non valide" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/hr.json b/homeassistant/components/airq/translations/hr.json new file mode 100644 index 00000000000..aedbaf7ed09 --- /dev/null +++ b/homeassistant/components/airq/translations/hr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran" + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo", + "invalid_auth": "Neva\u017ee\u0107a provjera autenti\u010dnosti", + "invalid_input": "Neva\u017ee\u0107i naziv hosta ili IP adresa" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa", + "password": "Lozinka" + }, + "description": "Navedite IP adresu ili mDNS ure\u0111aja i njegovu lozinku", + "title": "Identificirajte ure\u0111aj" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/hu.json b/homeassistant/components/airq/translations/hu.json new file mode 100644 index 00000000000..adfcb73e289 --- /dev/null +++ b/homeassistant/components/airq/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_input": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm", + "password": "Jelsz\u00f3" + }, + "description": "Adja meg az eszk\u00f6z IP-c\u00edm\u00e9t vagy mDNS c\u00edm\u00e9t \u00e9s jelszav\u00e1t.", + "title": "Az eszk\u00f6z azonos\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/id.json b/homeassistant/components/airq/translations/id.json new file mode 100644 index 00000000000..db210a13491 --- /dev/null +++ b/homeassistant/components/airq/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_input": "Nama host atau alamat IP tidak valid" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "password": "Kata Sandi" + }, + "description": "Berikan alamat IP atau mDNS perangkat dan kata sandinya", + "title": "Identifikasi perangkat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/it.json b/homeassistant/components/airq/translations/it.json new file mode 100644 index 00000000000..782f631bb09 --- /dev/null +++ b/homeassistant/components/airq/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_input": "Nome host o indirizzo IP non valido" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP", + "password": "Password" + }, + "description": "Fornire l'indirizzo IP o mDNS del dispositivo e la relativa password", + "title": "Identifica il dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/ja.json b/homeassistant/components/airq/translations/ja.json new file mode 100644 index 00000000000..8bbc8791dde --- /dev/null +++ b/homeassistant/components/airq/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_input": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" + }, + "step": { + "user": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/nl.json b/homeassistant/components/airq/translations/nl.json new file mode 100644 index 00000000000..07fe49d3708 --- /dev/null +++ b/homeassistant/components/airq/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt", + "invalid_auth": "Ongeldige authenticatie poging", + "invalid_input": "Ongeldige hostnaam of IP adres" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adres", + "password": "Wachtwoord" + }, + "description": "Geef het IP adress of mDNS van het apparaat en het bijbehorend wachtwoord", + "title": "Identificeer het apparaat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/no.json b/homeassistant/components/airq/translations/no.json new file mode 100644 index 00000000000..00f07077294 --- /dev/null +++ b/homeassistant/components/airq/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_input": "Ugyldig vertsnavn eller IP-adresse" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse", + "password": "Passord" + }, + "description": "Oppgi IP-adressen eller mDNS til enheten og passordet", + "title": "Identifiser enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/pl.json b/homeassistant/components/airq/translations/pl.json new file mode 100644 index 00000000000..bf64c7906d9 --- /dev/null +++ b/homeassistant/components/airq/translations/pl.json @@ -0,0 +1,22 @@ +{ + "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_input": "Nieprawid\u0142owa nazwa hosta lub adres IP" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP", + "password": "Has\u0142o" + }, + "description": "Podaj adres IP lub mDNS urz\u0105dzenia i jego has\u0142o", + "title": "Zidentyfikuj urz\u0105dzenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/pt-BR.json b/homeassistant/components/airq/translations/pt-BR.json new file mode 100644 index 00000000000..614b80c4b0e --- /dev/null +++ b/homeassistant/components/airq/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_input": "Nome de host ou endere\u00e7o IP inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "password": "Senha" + }, + "description": "Forne\u00e7a o endere\u00e7o IP ou mDNS do dispositivo e sua senha", + "title": "Identifique o dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/ru.json b/homeassistant/components/airq/translations/ru.json new file mode 100644 index 00000000000..b2456f01a33 --- /dev/null +++ b/homeassistant/components/airq/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." + }, + "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_input": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 mDNS \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u0435\u0433\u043e \u043f\u0430\u0440\u043e\u043b\u044c.", + "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/sk.json b/homeassistant/components/airq/translations/sk.json new file mode 100644 index 00000000000..6598d0f17af --- /dev/null +++ b/homeassistant/components/airq/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_input": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa", + "password": "Heslo" + }, + "description": "Zadajte IP adresu alebo mDNS zariadenia a jeho heslo", + "title": "Identifikujte zariadenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/zh-Hant.json b/homeassistant/components/airq/translations/zh-Hant.json new file mode 100644 index 00000000000..db4d2d8fcf0 --- /dev/null +++ b/homeassistant/components/airq/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_input": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc" + }, + "description": "\u63d0\u4f9b\u88dd\u7f6e\u4e4b IP \u4f4d\u5740\u6216 mDNS \u53ca\u5bc6\u78bc", + "title": "\u8b58\u5225\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/sk.json b/homeassistant/components/airthings/translations/sk.json index 5ada995aa6e..cc0b6f42502 100644 --- a/homeassistant/components/airthings/translations/sk.json +++ b/homeassistant/components/airthings/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "description": "Ak chcete n\u00e1js\u0165 svoje poverenia, prihl\u00e1ste sa na adrese {url}", + "id": "ID", + "secret": "Tajn\u00e9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/he.json b/homeassistant/components/airthings_ble/translations/he.json index 3ba358c4465..467b7ec0499 100644 --- a/homeassistant/components/airthings_ble/translations/he.json +++ b/homeassistant/components/airthings_ble/translations/he.json @@ -4,7 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name}", diff --git a/homeassistant/components/airthings_ble/translations/hy.json b/homeassistant/components/airthings_ble/translations/hy.json new file mode 100644 index 00000000000..6284ab97571 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/hy.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u0549\u0570\u0561\u057b\u0578\u0572\u057e\u0565\u0581 \u0574\u056b\u0561\u0576\u0561\u056c", + "no_devices_found": "\u0551\u0561\u0576\u0581\u0578\u0582\u0574 \u057d\u0561\u0580\u0584\u0565\u0580 \u0579\u0565\u0576 \u0563\u057f\u0576\u057e\u0565\u056c", + "unknown": "\u0531\u0576\u057d\u057a\u0561\u057d\u0565\u056c\u056b \u057d\u056d\u0561\u056c" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0551\u0561\u0576\u056f\u0561\u0576\u0578\u0582\u055e\u0574 \u0565\u0584 \u056f\u0561\u0580\u0563\u0561\u057e\u0578\u0580\u0565\u056c {name}-\u0568:" + }, + "user": { + "description": "\u0538\u0576\u057f\u0580\u0565\u0584 \u057d\u0561\u0580\u0584\u0568 \u056f\u0561\u0580\u0563\u0561\u057e\u0578\u0580\u0565\u056c\u0578\u0582 \u0570\u0561\u0574\u0561\u0580" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/nl.json b/homeassistant/components/airthings_ble/translations/nl.json index 19c9a433f99..785b57f8bb8 100644 --- a/homeassistant/components/airthings_ble/translations/nl.json +++ b/homeassistant/components/airthings_ble/translations/nl.json @@ -5,18 +5,18 @@ "already_in_progress": "Nederlands", "cannot_connect": "Nederlands", "no_devices_found": "Nederlands", - "unknown": "Nederlands" + "unknown": "Onverwachte fout" }, "flow_title": "Nederlands", "step": { "bluetooth_confirm": { - "description": "Nederlands" + "description": "Wilt u {name} instellen?" }, "user": { "data": { - "address": "Nederlands" + "address": "Apparaat" }, - "description": "Nederlands" + "description": "Kies een apparaat om in te stellen" } } } diff --git a/homeassistant/components/airthings_ble/translations/sk.json b/homeassistant/components/airthings_ble/translations/sk.json new file mode 100644 index 00000000000..8643ee69fa7 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/sk.json b/homeassistant/components/airtouch4/translations/sk.json new file mode 100644 index 00000000000..aaab8c373dc --- /dev/null +++ b/homeassistant/components/airtouch4/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_units": "Nepodarilo sa n\u00e1js\u0165 \u017eiadne skupiny AirTouch 4." + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + }, + "title": "Nastavte podrobnosti pripojenia AirTouch 4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 9510c938cb0..8ba75c43bdb 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -17,7 +17,7 @@ from pyairvisual.node import NodeProError import voluptuous as vol from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry, OptionsFlow +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_IP_ADDRESS, @@ -30,6 +30,10 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from . import async_get_geography_id from .const import ( @@ -66,6 +70,13 @@ PICK_INTEGRATION_TYPE_SCHEMA = vol.Schema( } ) +OPTIONS_SCHEMA = vol.Schema( + {vol.Required(CONF_SHOW_ON_MAP): bool}, +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an AirVisual config flow.""" @@ -165,9 +176,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: """Define the config flow to handle options.""" - return AirVisualOptionsFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_geography_by_coords( self, user_input: dict[str, str] | None = None @@ -258,30 +269,3 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input["type"] == INTEGRATION_TYPE_GEOGRAPHY_NAME: return await self.async_step_geography_by_name() return await self.async_step_node_pro() - - -class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): - """Handle an AirVisual options flow.""" - - def __init__(self, entry: ConfigEntry) -> None: - """Initialize.""" - self.entry = entry - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> FlowResult: - """Manage the options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Required( - CONF_SHOW_ON_MAP, - default=self.entry.options.get(CONF_SHOW_ON_MAP), - ): bool - } - ), - ) diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index 807b9556240..d97a78ab191 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/airvisual/translations/sensor.sk.json b/homeassistant/components/airvisual/translations/sensor.sk.json index 6d640d965b9..b550f1e6629 100644 --- a/homeassistant/components/airvisual/translations/sensor.sk.json +++ b/homeassistant/components/airvisual/translations/sensor.sk.json @@ -1,7 +1,20 @@ { "state": { + "airvisual__pollutant_label": { + "co": "Oxid uho\u013enat\u00fd", + "n2": "Oxid dusi\u010dit\u00fd", + "o3": "Oz\u00f3n", + "p1": "PM10", + "p2": "PM2,5", + "s2": "Oxid siri\u010dit\u00fd" + }, "airvisual__pollutant_level": { - "good": "Dobr\u00e9" + "good": "Dobr\u00e9", + "hazardous": "Nebezpe\u010dn\u00e9", + "moderate": "Mierne", + "unhealthy": "Nezdrav\u00e9", + "unhealthy_sensitive": "Nezdrav\u00e9 pre citliv\u00e9 skupiny", + "very_unhealthy": "Ve\u013emi nezdrav\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sk.json b/homeassistant/components/airvisual/translations/sk.json index 22c02bbfec3..23301cf1742 100644 --- a/homeassistant/components/airvisual/translations/sk.json +++ b/homeassistant/components/airvisual/translations/sk.json @@ -1,10 +1,14 @@ { "config": { "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9 alebo ID uzla/Pro je u\u017e zaregistrovan\u00e9.", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "general_error": "Neo\u010dak\u00e1van\u00e1 chyba", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "location_not_found": "Poloha sa nena\u0161la" }, "step": { "geography_by_coords": { @@ -12,17 +16,42 @@ "api_key": "API k\u013e\u00fa\u010d", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", "longitude": "Zemepisn\u00e1 d\u013a\u017eka" - } + }, + "title": "Konfigur\u00e1cia geografie" }, "geography_by_name": { "data": { - "api_key": "API k\u013e\u00fa\u010d" + "api_key": "API k\u013e\u00fa\u010d", + "city": "Mesto", + "country": "Krajina", + "state": "stav" + }, + "title": "Konfigur\u00e1cia geografie" + }, + "node_pro": { + "data": { + "ip_address": "Hostite\u013e", + "password": "Heslo" } }, "reauth_confirm": { "data": { "api_key": "API k\u013e\u00fa\u010d" - } + }, + "title": "Op\u00e4tovn\u00e9 overenie AirVisual" + }, + "user": { + "title": "Nakonfigurujte AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Zobrazte na mape sledovan\u00fa geografiu" + }, + "title": "Nakonfigurujte AirVisual" } } } diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index 89a2d7f1f9e..7a8fdbf884b 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Airzone.""" from __future__ import annotations +import logging from typing import Any from aioairzone.const import DEFAULT_PORT, DEFAULT_SYSTEM_ID @@ -9,13 +10,16 @@ from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, @@ -29,9 +33,17 @@ SYSTEM_ID_SCHEMA = CONFIG_SCHEMA.extend( ) +def short_mac(addr: str) -> str: + """Convert MAC address to short address.""" + return addr.replace(":", "")[-4:].upper() + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle config flow for an Airzone device.""" + _discovered_ip: str | None = None + _discovered_mac: str | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -60,7 +72,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" else: if mac: - await self.async_set_unique_id(format_mac(mac)) + await self.async_set_unique_id( + format_mac(mac), raise_on_progress=False + ) self._abort_if_unique_id_configured( updates={ CONF_HOST: user_input[CONF_HOST], @@ -76,3 +90,78 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=data_schema, errors=errors, ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle DHCP discovery.""" + self._discovered_ip = discovery_info.ip + self._discovered_mac = discovery_info.macaddress + + _LOGGER.debug( + "DHCP discovery detected Airzone WebServer: %s", self._discovered_mac + ) + + self._async_abort_entries_match({CONF_HOST: self._discovered_ip}) + + await self.async_set_unique_id(format_mac(self._discovered_mac)) + self._abort_if_unique_id_configured() + + options = ConnectionOptions(self._discovered_ip) + airzone = AirzoneLocalApi( + aiohttp_client.async_get_clientsession(self.hass), options + ) + try: + await airzone.get_version() + except AirzoneError as err: + raise AbortFlow("cannot_connect") from err + + return await self.async_step_discovered_connection() + + async def async_step_discovered_connection( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_ip is not None + assert self._discovered_mac is not None + + errors = {} + base_schema = {vol.Required(CONF_PORT, default=DEFAULT_PORT): int} + + if user_input is not None: + airzone = AirzoneLocalApi( + aiohttp_client.async_get_clientsession(self.hass), + ConnectionOptions( + self._discovered_ip, + user_input[CONF_PORT], + user_input.get(CONF_ID, DEFAULT_SYSTEM_ID), + ), + ) + + try: + mac = await airzone.validate() + except InvalidSystem: + base_schema[vol.Required(CONF_ID, default=1)] = int + errors[CONF_ID] = "invalid_system_id" + except AirzoneError: + errors["base"] = "cannot_connect" + else: + user_input[CONF_HOST] = self._discovered_ip + + if mac is None: + mac = self._discovered_mac + + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + ) + + title = f"Airzone {short_mac(mac)}" + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="discovered_connection", + data_schema=vol.Schema(base_schema), + errors=errors, + ) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index bd8ed6b9920..142ace5e70b 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,8 +3,13 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.8"], + "requirements": ["aioairzone==0.5.1"], "codeowners": ["@Noltari"], "iot_class": "local_polling", - "loggers": ["aioairzone"] + "loggers": ["aioairzone"], + "dhcp": [ + { + "macaddress": "E84F25*" + } + ] } diff --git a/homeassistant/components/airzone/strings.json b/homeassistant/components/airzone/strings.json index 855b5615482..306e63da36c 100644 --- a/homeassistant/components/airzone/strings.json +++ b/homeassistant/components/airzone/strings.json @@ -8,12 +8,19 @@ "invalid_system_id": "Invalid Airzone System ID" }, "step": { - "user": { + "discovered_connection": { "data": { + "id": "[%key:component::airzone::config::step::user::data::id%]", "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" - }, - "description": "Set up Airzone integration." + } + }, + "user": { + "data": { + "id": "System ID", + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } } } } diff --git a/homeassistant/components/airzone/translations/bg.json b/homeassistant/components/airzone/translations/bg.json index cc5f200ef95..3cc33b64ef5 100644 --- a/homeassistant/components/airzone/translations/bg.json +++ b/homeassistant/components/airzone/translations/bg.json @@ -7,9 +7,17 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "discovered_connection": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "id": "ID \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430", + "port": "\u041f\u043e\u0440\u0442" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", + "id": "ID \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/airzone/translations/ca.json b/homeassistant/components/airzone/translations/ca.json index 111a53fcf3a..4f37cf005f3 100644 --- a/homeassistant/components/airzone/translations/ca.json +++ b/homeassistant/components/airzone/translations/ca.json @@ -8,9 +8,17 @@ "invalid_system_id": "ID de sistema Airzone inv\u00e0lid" }, "step": { + "discovered_connection": { + "data": { + "host": "Amfitri\u00f3", + "id": "ID de sistema", + "port": "Port" + } + }, "user": { "data": { "host": "Amfitri\u00f3", + "id": "ID de sistema", "port": "Port" }, "description": "Configura la integraci\u00f3 Airzone." diff --git a/homeassistant/components/airzone/translations/cs.json b/homeassistant/components/airzone/translations/cs.json index aad89c1cbe3..9015e7fd0b6 100644 --- a/homeassistant/components/airzone/translations/cs.json +++ b/homeassistant/components/airzone/translations/cs.json @@ -7,9 +7,17 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { + "discovered_connection": { + "data": { + "host": "Hostitel", + "id": "ID syst\u00e9mu", + "port": "Port" + } + }, "user": { "data": { "host": "Hostitel", + "id": "ID syst\u00e9mu", "port": "Port" } } diff --git a/homeassistant/components/airzone/translations/de.json b/homeassistant/components/airzone/translations/de.json index 2edb50330f4..38b56e70308 100644 --- a/homeassistant/components/airzone/translations/de.json +++ b/homeassistant/components/airzone/translations/de.json @@ -5,15 +5,23 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_system_id": "Ung\u00fcltige Airzone-System-ID" + "invalid_system_id": "Ung\u00fcltige Airzone System-ID" }, "step": { + "discovered_connection": { + "data": { + "host": "Host", + "id": "System-ID", + "port": "Port" + } + }, "user": { "data": { "host": "Host", + "id": "System-ID", "port": "Port" }, - "description": "Richte die Airzone-Integration ein." + "description": "Richte die Airzone Integration ein." } } } diff --git a/homeassistant/components/airzone/translations/el.json b/homeassistant/components/airzone/translations/el.json index 13da6efe27d..2da19364c28 100644 --- a/homeassistant/components/airzone/translations/el.json +++ b/homeassistant/components/airzone/translations/el.json @@ -8,9 +8,17 @@ "invalid_system_id": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 Airzone" }, "step": { + "discovered_connection": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + } + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", "port": "\u0398\u03cd\u03c1\u03b1" }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Airzone." diff --git a/homeassistant/components/airzone/translations/en.json b/homeassistant/components/airzone/translations/en.json index 85da81afd55..638f542e1ca 100644 --- a/homeassistant/components/airzone/translations/en.json +++ b/homeassistant/components/airzone/translations/en.json @@ -8,9 +8,17 @@ "invalid_system_id": "Invalid Airzone System ID" }, "step": { + "discovered_connection": { + "data": { + "host": "Host", + "id": "System ID", + "port": "Port" + } + }, "user": { "data": { "host": "Host", + "id": "System ID", "port": "Port" }, "description": "Set up Airzone integration." diff --git a/homeassistant/components/airzone/translations/es.json b/homeassistant/components/airzone/translations/es.json index 20c05fa714f..858cb488344 100644 --- a/homeassistant/components/airzone/translations/es.json +++ b/homeassistant/components/airzone/translations/es.json @@ -8,9 +8,17 @@ "invalid_system_id": "ID del sistema Airzone no v\u00e1lido" }, "step": { + "discovered_connection": { + "data": { + "host": "Host", + "id": "ID del sistema", + "port": "Puerto" + } + }, "user": { "data": { "host": "Host", + "id": "ID del sistema", "port": "Puerto" }, "description": "Configura la integraci\u00f3n Airzone." diff --git a/homeassistant/components/airzone/translations/et.json b/homeassistant/components/airzone/translations/et.json index dff9d1173f6..c209a688c2d 100644 --- a/homeassistant/components/airzone/translations/et.json +++ b/homeassistant/components/airzone/translations/et.json @@ -8,9 +8,17 @@ "invalid_system_id": "Airzone'i s\u00fcsteemi ID on vigane" }, "step": { + "discovered_connection": { + "data": { + "host": "Host", + "id": "S\u00fcsteemi ID", + "port": "Port" + } + }, "user": { "data": { "host": "Host", + "id": "S\u00fcsteemi ID", "port": "Port" }, "description": "Seadista Airzone'i sidumine" diff --git a/homeassistant/components/airzone/translations/fr.json b/homeassistant/components/airzone/translations/fr.json index 40d22ad8bd9..f525617b2ef 100644 --- a/homeassistant/components/airzone/translations/fr.json +++ b/homeassistant/components/airzone/translations/fr.json @@ -8,9 +8,17 @@ "invalid_system_id": "ID syst\u00e8me Airzone non valide" }, "step": { + "discovered_connection": { + "data": { + "host": "H\u00f4te", + "id": "ID syst\u00e8me", + "port": "Port" + } + }, "user": { "data": { "host": "H\u00f4te", + "id": "ID syst\u00e8me", "port": "Port" }, "description": "Configurer l'int\u00e9gration Airzone." diff --git a/homeassistant/components/airzone/translations/id.json b/homeassistant/components/airzone/translations/id.json index ec37639811b..f80012a3ad4 100644 --- a/homeassistant/components/airzone/translations/id.json +++ b/homeassistant/components/airzone/translations/id.json @@ -8,9 +8,17 @@ "invalid_system_id": "ID Sistem Airzone Tidak Valid" }, "step": { + "discovered_connection": { + "data": { + "host": "Host", + "id": "ID Sistem", + "port": "Port" + } + }, "user": { "data": { "host": "Host", + "id": "ID Sistem", "port": "Port" }, "description": "Siapkan integrasi Airzone" diff --git a/homeassistant/components/airzone/translations/it.json b/homeassistant/components/airzone/translations/it.json index 32f8c67b545..10297d1e288 100644 --- a/homeassistant/components/airzone/translations/it.json +++ b/homeassistant/components/airzone/translations/it.json @@ -8,9 +8,17 @@ "invalid_system_id": "ID sistema Airzone non valido" }, "step": { + "discovered_connection": { + "data": { + "host": "Host", + "id": "ID di sistema", + "port": "Porta" + } + }, "user": { "data": { "host": "Host", + "id": "ID di sistema", "port": "Porta" }, "description": "Imposta l'integrazione Airzone." diff --git a/homeassistant/components/airzone/translations/nl.json b/homeassistant/components/airzone/translations/nl.json index e182d71963d..b87f0f3cab5 100644 --- a/homeassistant/components/airzone/translations/nl.json +++ b/homeassistant/components/airzone/translations/nl.json @@ -8,9 +8,16 @@ "invalid_system_id": "Ongeldige Airzone systeem ID" }, "step": { + "discovered_connection": { + "data": { + "id": "Systeem ID", + "port": "Poort" + } + }, "user": { "data": { "host": "Host", + "id": "Systeem ID", "port": "Poort" }, "description": "Airzone integratie instellen." diff --git a/homeassistant/components/airzone/translations/no.json b/homeassistant/components/airzone/translations/no.json index 6eeaee3a53a..2ab24253236 100644 --- a/homeassistant/components/airzone/translations/no.json +++ b/homeassistant/components/airzone/translations/no.json @@ -8,9 +8,17 @@ "invalid_system_id": "Ugyldig Airzone System ID" }, "step": { + "discovered_connection": { + "data": { + "host": "Vert", + "id": "System-ID", + "port": "Port" + } + }, "user": { "data": { "host": "Vert", + "id": "System-ID", "port": "Port" }, "description": "Sett opp Airzone-integrasjon." diff --git a/homeassistant/components/airzone/translations/pl.json b/homeassistant/components/airzone/translations/pl.json index e389618ff80..42efddb4310 100644 --- a/homeassistant/components/airzone/translations/pl.json +++ b/homeassistant/components/airzone/translations/pl.json @@ -8,9 +8,17 @@ "invalid_system_id": "Nieprawid\u0142owy identyfikator systemu Airzone" }, "step": { + "discovered_connection": { + "data": { + "host": "Nazwa hosta lub adres IP", + "id": "Identyfikator systemu", + "port": "Port" + } + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", + "id": "Identyfikator systemu", "port": "Port" }, "description": "Skonfiguruj integracj\u0119 Airzone." diff --git a/homeassistant/components/airzone/translations/pt-BR.json b/homeassistant/components/airzone/translations/pt-BR.json index c2668c937b4..10a0e4555b3 100644 --- a/homeassistant/components/airzone/translations/pt-BR.json +++ b/homeassistant/components/airzone/translations/pt-BR.json @@ -8,9 +8,17 @@ "invalid_system_id": "ID do sistema Airzone inv\u00e1lido" }, "step": { + "discovered_connection": { + "data": { + "host": "Host", + "id": "ID do sistema", + "port": "Porta" + } + }, "user": { "data": { "host": "Nome do host", + "id": "ID do sistema", "port": "Porta" }, "description": "Configure a integra\u00e7\u00e3o Airzone." diff --git a/homeassistant/components/airzone/translations/ru.json b/homeassistant/components/airzone/translations/ru.json index d480866b262..6ee7cb98950 100644 --- a/homeassistant/components/airzone/translations/ru.json +++ b/homeassistant/components/airzone/translations/ru.json @@ -8,9 +8,17 @@ "invalid_system_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0438\u0441\u0442\u0435\u043c\u044b Airzone." }, "step": { + "discovered_connection": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "id": "ID \u0441\u0438\u0441\u0442\u0435\u043c\u044b", + "port": "\u041f\u043e\u0440\u0442" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", + "id": "ID \u0441\u0438\u0441\u0442\u0435\u043c\u044b", "port": "\u041f\u043e\u0440\u0442" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Airzone." diff --git a/homeassistant/components/airzone/translations/sk.json b/homeassistant/components/airzone/translations/sk.json new file mode 100644 index 00000000000..2566a9e353e --- /dev/null +++ b/homeassistant/components/airzone/translations/sk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_system_id": "Neplatn\u00e9 ID syst\u00e9mu Airzone" + }, + "step": { + "discovered_connection": { + "data": { + "host": "Hostite\u013e", + "id": "ID syst\u00e9mu", + "port": "Port" + } + }, + "user": { + "data": { + "host": "Hostite\u013e", + "id": "ID syst\u00e9mu", + "port": "Port" + }, + "description": "Nastavte integr\u00e1ciu Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/zh-Hans.json b/homeassistant/components/airzone/translations/zh-Hans.json new file mode 100644 index 00000000000..17253bcf52f --- /dev/null +++ b/homeassistant/components/airzone/translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "id": "System ID", + "port": "\u7aef\u53e3\u53f7" + } + }, + "user": { + "data": { + "id": "System ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/zh-Hant.json b/homeassistant/components/airzone/translations/zh-Hant.json index 42166fe39ec..b53c19ad364 100644 --- a/homeassistant/components/airzone/translations/zh-Hant.json +++ b/homeassistant/components/airzone/translations/zh-Hant.json @@ -8,9 +8,17 @@ "invalid_system_id": "\u7121\u6548\u7684 Airzone \u7cfb\u7d71 ID" }, "step": { + "discovered_connection": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "id": "\u7cfb\u7d71 ID", + "port": "\u901a\u8a0a\u57e0" + } + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", + "id": "\u7cfb\u7d71 ID", "port": "\u901a\u8a0a\u57e0" }, "description": "\u8a2d\u5b9a Airzone \u6574\u5408\u3002" diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 6888eb4d8b2..71ad99a640d 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.47"], + "requirements": ["AIOAladdinConnect==0.1.48"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/translations/bg.json b/homeassistant/components/aladdin_connect/translations/bg.json index d5babb02c1c..0a5cd10febe 100644 --- a/homeassistant/components/aladdin_connect/translations/bg.json +++ b/homeassistant/components/aladdin_connect/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/aladdin_connect/translations/sk.json b/homeassistant/components/aladdin_connect/translations/sk.json index c9a7b4c204a..030604ec1bc 100644 --- a/homeassistant/components/aladdin_connect/translations/sk.json +++ b/homeassistant/components/aladdin_connect/translations/sk.json @@ -1,9 +1,25 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie" + }, "step": { "reauth_confirm": { "data": { "password": "Heslo" + }, + "description": "Integr\u00e1cia Aladdin Connect potrebuje op\u00e4tovn\u00e9 overenie v\u00e1\u0161ho \u00fa\u010dtu", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 4d74a39d977..f3e02465c13 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -133,7 +133,9 @@ class AlarmControlPanelEntity(Entity): _attr_changed_by: str | None = None _attr_code_arm_required: bool = True _attr_code_format: CodeFormat | None = None - _attr_supported_features: int + _attr_supported_features: AlarmControlPanelEntityFeature = ( + AlarmControlPanelEntityFeature(0) + ) @property def code_format(self) -> CodeFormat | None: @@ -207,7 +209,7 @@ class AlarmControlPanelEntity(Entity): await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) @property - def supported_features(self) -> int: + def supported_features(self) -> AlarmControlPanelEntityFeature: """Return the list of supported features.""" return self._attr_supported_features diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index 3a2f2c51551..e6e628f834d 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -1,5 +1,5 @@ """Provides the constants needed for component.""" -from enum import IntEnum +from enum import IntFlag from typing import Final from homeassistant.backports.enum import StrEnum @@ -23,7 +23,7 @@ FORMAT_TEXT: Final = "text" FORMAT_NUMBER: Final = "number" -class AlarmControlPanelEntityFeature(IntEnum): +class AlarmControlPanelEntityFeature(IntFlag): """Supported features of the alarm control panel entity.""" ARM_HOME = 1 diff --git a/homeassistant/components/alarm_control_panel/translations/is.json b/homeassistant/components/alarm_control_panel/translations/is.json index 16c2aeec21d..eb962bc6626 100644 --- a/homeassistant/components/alarm_control_panel/translations/is.json +++ b/homeassistant/components/alarm_control_panel/translations/is.json @@ -4,7 +4,8 @@ "is_armed_away": "{entity_name} er \u00e1 ver\u00f0i \u00fati", "is_armed_home": "{entity_name} er \u00e1 ver\u00f0i heima", "is_armed_night": "{entity_name} er \u00e1 ver\u00f0i n\u00f3tt", - "is_disarmed": "{entity_name} er ekki \u00e1 ver\u00f0i" + "is_disarmed": "{entity_name} er ekki \u00e1 ver\u00f0i", + "is_triggered": "{entity_name} er r\u00e6st" }, "trigger_type": { "armed_away": "{entity_name} \u00e1 ver\u00f0i \u00fati", diff --git a/homeassistant/components/alarm_control_panel/translations/sk.json b/homeassistant/components/alarm_control_panel/translations/sk.json index ceff70c00a6..844fd5c2ec1 100644 --- a/homeassistant/components/alarm_control_panel/translations/sk.json +++ b/homeassistant/components/alarm_control_panel/translations/sk.json @@ -1,4 +1,12 @@ { + "device_automation": { + "action_type": { + "trigger": "Sp\u00fa\u0161\u0165a\u010d {entity_name}" + }, + "trigger_type": { + "triggered": "{entity_name} spusten\u00fd" + } + }, "state": { "_": { "armed": "Akt\u00edvny", diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index f324772c909..73c1c82b6c1 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -66,7 +66,7 @@ "data": { "zone_number": "Zonennummer" }, - "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest.", + "description": "Gib die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest.", "title": "AlarmDecoder konfigurieren" } } diff --git a/homeassistant/components/alarmdecoder/translations/sk.json b/homeassistant/components/alarmdecoder/translations/sk.json index 9b801344831..07bceeca884 100644 --- a/homeassistant/components/alarmdecoder/translations/sk.json +++ b/homeassistant/components/alarmdecoder/translations/sk.json @@ -1,11 +1,65 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "create_entry": { + "default": "\u00daspe\u0161ne pripojen\u00e9 k AlarmDecoder." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "protocol": { "data": { + "device_baudrate": "Prenosov\u00e1 r\u00fdchlos\u0165 zariadenia", + "device_path": "Cesta k zariadeniu", + "host": "Hostite\u013e", "port": "Port" + }, + "title": "Nakonfigurujte nastavenia pripojenia" + }, + "user": { + "data": { + "protocol": "Protokol" } } } + }, + "options": { + "error": { + "int": "Pole ni\u017e\u0161ie mus\u00ed by\u0165 cel\u00e9 \u010d\u00edslo.", + "loop_range": "RF Loop mus\u00ed by\u0165 cel\u00e9 \u010d\u00edslo od 1 do 4." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternat\u00edvny no\u010dn\u00fd re\u017eim" + } + }, + "init": { + "data": { + "edit_select": "Upravi\u0165" + }, + "description": "\u010co by ste chceli upravi\u0165?" + }, + "zone_details": { + "data": { + "zone_loop": "RF slu\u010dka", + "zone_name": "N\u00e1zov z\u00f3ny", + "zone_relayaddr": "Adresa rel\u00e9", + "zone_relaychan": "Rel\u00e9ov\u00fd kan\u00e1l", + "zone_rfid": "RF Serial", + "zone_type": "Typ z\u00f3ny" + }, + "description": "Zadajte podrobnosti pre z\u00f3nu {zone_number}. Ak chcete odstr\u00e1ni\u0165 z\u00f3nu {zone_number}, ponechajte n\u00e1zov z\u00f3ny pr\u00e1zdny." + }, + "zone_select": { + "data": { + "zone_number": "\u010c\u00edslo z\u00f3ny" + }, + "description": "Zadajte \u010d\u00edslo z\u00f3ny, ktor\u00fa chcete prida\u0165, upravi\u0165 alebo odstr\u00e1ni\u0165." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index d28ff5dc804..0008ba26f8a 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -1,4 +1,8 @@ """Support for Alexa skill service end point.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant.const import ( @@ -87,18 +91,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config = config[DOMAIN] - flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS) - intent.async_setup(hass) - if flash_briefings_config: + if flash_briefings_config := config.get(CONF_FLASH_BRIEFINGS): flash_briefings.async_setup(hass, flash_briefings_config) - try: - smart_home_config = config[CONF_SMART_HOME] - except KeyError: - pass - else: + # smart_home being absent is not the same as smart_home being None + if CONF_SMART_HOME in config: + smart_home_config: dict[str, Any] | None = config[CONF_SMART_HOME] smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) await smart_home_http.async_setup(hass, smart_home_config) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 15870c7bbfa..56b9e88e27e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -8,6 +8,7 @@ from homeassistant.components import ( climate, cover, fan, + humidifier, image_processing, input_button, input_number, @@ -398,6 +399,8 @@ class AlexaPowerController(AlexaCapability): is_on = self.entity.state != climate.HVACMode.OFF elif self.entity.domain == fan.DOMAIN: is_on = self.entity.state == fan.STATE_ON + elif self.entity.domain == humidifier.DOMAIN: + is_on = self.entity.state == humidifier.STATE_ON elif self.entity.domain == vacuum.DOMAIN: is_on = self.entity.state == vacuum.STATE_CLEANING elif self.entity.domain == timer.DOMAIN: @@ -653,59 +656,6 @@ class AlexaColorTemperatureController(AlexaCapability): return None -class AlexaPercentageController(AlexaCapability): - """Implements Alexa.PercentageController. - - https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html - """ - - supported_locales = { - "de-DE", - "en-AU", - "en-CA", - "en-GB", - "en-IN", - "en-US", - "es-ES", - "es-US", - "fr-CA", - "fr-FR", - "hi-IN", - "it-IT", - "ja-JP", - "pt-BR", - } - - def name(self): - """Return the Alexa API name of this interface.""" - return "Alexa.PercentageController" - - def properties_supported(self): - """Return what properties this entity supports.""" - return [{"name": "percentage"}] - - def properties_proactively_reported(self): - """Return True if properties asynchronously reported.""" - return True - - def properties_retrievable(self): - """Return True if properties can be retrieved.""" - return True - - def get_property(self, name): - """Read and return a property.""" - if name != "percentage": - raise UnsupportedProperty(name) - - if self.entity.domain == fan.DOMAIN: - return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 - - if self.entity.domain == cover.DOMAIN: - return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0) - - return 0 - - class AlexaSpeaker(AlexaCapability): """Implements Alexa.Speaker. @@ -1403,6 +1353,12 @@ class AlexaModeController(AlexaCapability): if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None): return f"{fan.ATTR_PRESET_MODE}.{mode}" + # Humidifier mode + if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": + mode = self.entity.attributes.get(humidifier.ATTR_MODE, None) + if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []): + return f"{humidifier.ATTR_MODE}.{mode}" + # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": # Return state instead of position when using ModeController. @@ -1459,6 +1415,20 @@ class AlexaModeController(AlexaCapability): ) return self._resource.serialize_capability_resources() + # Humidifier modes + if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": + self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False) + modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []) + for mode in modes: + self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode]) + # Humidifiers or Fans with a single mode completely break Alexa discovery, add a + # fake preset (see issue #53832). + if len(modes) == 1: + self._resource.add_mode( + f"{humidifier.ATTR_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA] + ) + return self._resource.serialize_capability_resources() + # Cover Position Resources if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": self._resource = AlexaModeResource( @@ -1600,6 +1570,12 @@ class AlexaRangeController(AlexaCapability): return self.entity.attributes.get(fan.ATTR_PERCENTAGE) return 100 if self.entity.state == fan.STATE_ON else 0 + # Humidifier target humidity + if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": + # If the humidifier is turned off the target humidity attribute is not set. + # We return 0 to make clear we do not know the current value. + return self.entity.attributes.get(humidifier.ATTR_HUMIDITY, 0) + # Input Number Value if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": return float(self.entity.state) @@ -1640,6 +1616,17 @@ class AlexaRangeController(AlexaCapability): ) return self._resource.serialize_capability_resources() + # Humidifier Target Humidity Resources + if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": + self._resource = AlexaPresetResource( + labels=["Humidity", "Percentage", "Target humidity"], + min_value=self.entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10), + max_value=self.entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90), + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + # Cover Position Resources if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": self._resource = AlexaPresetResource( @@ -1764,6 +1751,22 @@ class AlexaRangeController(AlexaCapability): ) return self._semantics.serialize_semantics() + # Target Humidity Percentage + if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": + lower_labels = [AlexaSemantics.ACTION_LOWER] + raise_labels = [AlexaSemantics.ACTION_RAISE] + self._semantics = AlexaSemantics() + min_value = self.entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10) + max_value = self.entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90) + + self._semantics.add_action_to_directive( + lower_labels, "SetRangeValue", {"rangeValue": min_value} + ) + self._semantics.add_action_to_directive( + raise_labels, "SetRangeValue", {"rangeValue": max_value} + ) + return self._semantics.serialize_semantics() + return None diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index d1061720718..a34355b7ddb 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -83,7 +83,8 @@ API_THERMOSTAT_MODES_CUSTOM = { } API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} -# AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode +# AlexaModeController does not like a single mode for the fan preset or humidifier mode, +# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode PRESET_MODE_NA = "-" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e002969952a..35313573b19 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -15,6 +15,7 @@ from homeassistant.components import ( cover, fan, group, + humidifier, image_processing, input_boolean, input_button, @@ -100,6 +101,9 @@ class DisplayCategory: # to HDMI1. Applies to Scenes ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER" + # Indicates a device that cools the air in interior spaces. + AIR_CONDITIONER = "AIR_CONDITIONER" + # Indicates a device that emits pleasant odors and masks unpleasant odors in interior spaces. AIR_FRESHENER = "AIR_FRESHENER" @@ -583,6 +587,30 @@ class FanCapabilities(AlexaEntity): yield Alexa(self.hass) +@ENTITY_ADAPTERS.register(humidifier.DOMAIN) +class HumidifierCapabilities(AlexaEntity): + """Class to represent Humidifier capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & humidifier.HumidifierEntityFeature.MODES: + yield AlexaModeController( + self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}" + ) + yield AlexaRangeController( + self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}" + ) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + @ENTITY_ADAPTERS.register(lock.DOMAIN) class LockCapabilities(AlexaEntity): """Class to represent Lock capabilities.""" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index b4c842dd5b5..d9a2e7016e9 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -14,6 +14,7 @@ from homeassistant.components import ( cover, fan, group, + humidifier, input_button, input_number, light, @@ -154,6 +155,8 @@ async def async_api_turn_on( service = cover.SERVICE_OPEN_COVER elif domain == fan.DOMAIN: service = fan.SERVICE_TURN_ON + elif domain == humidifier.DOMAIN: + service = humidifier.SERVICE_TURN_ON elif domain == vacuum.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if ( @@ -201,6 +204,8 @@ async def async_api_turn_off( service = cover.SERVICE_CLOSE_COVER elif domain == fan.DOMAIN: service = fan.SERVICE_TURN_OFF + elif domain == humidifier.DOMAIN: + service = humidifier.SERVICE_TURN_OFF elif domain == vacuum.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if ( @@ -438,63 +443,6 @@ async def async_api_deactivate( ) -@HANDLERS.register(("Alexa.PercentageController", "SetPercentage")) -async def async_api_set_percentage( - hass: ha.HomeAssistant, - config: AbstractConfig, - directive: AlexaDirective, - context: ha.Context, -) -> AlexaResponse: - """Process a set percentage request.""" - entity = directive.entity - - if entity.domain != fan.DOMAIN: - raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) - - percentage = int(directive.payload["percentage"]) - service = fan.SERVICE_SET_PERCENTAGE - data = { - ATTR_ENTITY_ID: entity.entity_id, - fan.ATTR_PERCENTAGE: percentage, - } - - await hass.services.async_call( - entity.domain, service, data, blocking=False, context=context - ) - - return directive.response() - - -@HANDLERS.register(("Alexa.PercentageController", "AdjustPercentage")) -async def async_api_adjust_percentage( - hass: ha.HomeAssistant, - config: AbstractConfig, - directive: AlexaDirective, - context: ha.Context, -) -> AlexaResponse: - """Process an adjust percentage request.""" - entity = directive.entity - - if entity.domain != fan.DOMAIN: - raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) - - percentage_delta = int(directive.payload["percentageDelta"]) - current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 - # set percentage - percentage = min(100, max(0, percentage_delta + current)) - service = fan.SERVICE_SET_PERCENTAGE - data = { - ATTR_ENTITY_ID: entity.entity_id, - fan.ATTR_PERCENTAGE: percentage, - } - - await hass.services.async_call( - entity.domain, service, data, blocking=False, context=context - ) - - return directive.response() - - @HANDLERS.register(("Alexa.LockController", "Lock")) async def async_api_lock( hass: ha.HomeAssistant, @@ -1130,6 +1078,18 @@ async def async_api_set_mode( msg = f"Entity '{entity.entity_id}' does not support Preset '{preset_mode}'" raise AlexaInvalidValueError(msg) + # Humidifier mode + elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": + mode = mode.split(".")[1] + if mode != PRESET_MODE_NA and mode in entity.attributes.get( + humidifier.ATTR_AVAILABLE_MODES + ): + service = humidifier.SERVICE_SET_MODE + data[humidifier.ATTR_MODE] = mode + else: + msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'" + raise AlexaInvalidValueError(msg) + # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": position = mode.split(".")[1] @@ -1306,6 +1266,12 @@ async def async_api_set_range( else: service = fan.SERVICE_TURN_ON + # Humidifier target humidity + elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": + range_value = int(range_value) + service = humidifier.SERVICE_SET_HUMIDITY + data[humidifier.ATTR_HUMIDITY] = range_value + # Input Number Value elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": range_value = float(range_value) @@ -1414,6 +1380,26 @@ async def async_api_adjust_range( else: service = fan.SERVICE_TURN_OFF + # Humidifier target humidity + elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": + percentage_step = 5 + range_delta = ( + int(range_delta * percentage_step) + if range_delta_default + else int(range_delta) + ) + service = humidifier.SERVICE_SET_HUMIDITY + if not (current := entity.attributes.get(humidifier.ATTR_HUMIDITY)): + msg = f"Unable to determine {entity.entity_id} current target humidity" + raise AlexaInvalidValueError(msg) + min_value = entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10) + max_value = entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90) + percentage = response_value = min( + max_value, max(min_value, range_delta + current) + ) + if percentage: + data[humidifier.ATTR_HUMIDITY] = percentage + # Input Number Value elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": range_delta = float(range_delta) diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 486079b0313..d73fc3590bd 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alexa", "dependencies": ["http"], "after_dependencies": ["camera"], - "codeowners": ["@home-assistant/cloud", "@ochlocracy"], + "codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/almond/translations/sk.json b/homeassistant/components/almond/translations/sk.json new file mode 100644 index 00000000000..db82c552cb2 --- /dev/null +++ b/homeassistant/components/almond/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurova\u0165 dom\u00e1ceho asistenta na pripojenie k Almond poskytovan\u00e9mu doplnkom: {addon}?" + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amazon_polly/const.py b/homeassistant/components/amazon_polly/const.py index 3892204c47a..a0250938fb4 100644 --- a/homeassistant/components/amazon_polly/const.py +++ b/homeassistant/components/amazon_polly/const.py @@ -36,6 +36,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Aditi", # Hindi "Amy", "Aria", + "Arlet", # Catalan, Neural "Arthur", # English, Neural "Astrid", # Swedish "Ayanda", @@ -50,6 +51,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Cristiano", "Daniel", # German, Neural "Dora", # Icelandic + "Elin", # Swedish, Neural "Emma", # English "Enrique", "Ewa", @@ -58,7 +60,11 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Geraint", # English Welsh "Giorgio", "Gwyneth", # Welsh + "Hala", # Arabic (Gulf), Neural + "Hannah", # German (Austrian), Neural "Hans", + "Hiujin", # Chinese (Cantonese), Neural + "Ida", # Norwegian, Neural "Ines", # Portuguese, European "Ivy", "Jacek", @@ -66,10 +72,12 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Joanna", "Joey", "Justin", + "Kajal", # English (Indian)/Hindi (Bilingual ), Neural "Karl", "Kendra", "Kevin", "Kimberly", + "Laura", # Dutch, Neural "Lea", # French "Liam", # Canadian French, Neural "Liv", # Norwegian @@ -87,6 +95,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Mizuki", # Japanese "Naja", # Danish "Nicole", # English Australian + "Ola", # Polish, Neural "Olivia", # Female, Australian, Neural "Penelope", # Spanish US "Pedro", # Spanish US, Neural @@ -96,6 +105,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Russell", "Salli", # English "Seoyeon", # Korean + "Suvi", # Finnish "Takumi", "Tatyana", # Russian "Vicki", # German diff --git a/homeassistant/components/amberelectric/translations/sk.json b/homeassistant/components/amberelectric/translations/sk.json new file mode 100644 index 00000000000..9935c530ebc --- /dev/null +++ b/homeassistant/components/amberelectric/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_api_token": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "no_site": "Nebola poskytnut\u00e1 \u017eiadna str\u00e1nka", + "unknown_error": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_token": "API token", + "site_id": "ID lokality" + }, + "description": "Prejdite na {api_url} a vygenerujte k\u013e\u00fa\u010d API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/sk.json b/homeassistant/components/ambiclimate/translations/sk.json index c19b1a0b70c..d13178679a4 100644 --- a/homeassistant/components/ambiclimate/translations/sk.json +++ b/homeassistant/components/ambiclimate/translations/sk.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "access_token": "Nezn\u00e1ma chyba pri generovan\u00ed pr\u00edstupov\u00e9ho tokenu.", + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie." + }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "error": { + "no_token": "Neoveren\u00e9 pomocou Ambiclimate" } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/zh-Hans.json b/homeassistant/components/ambient_station/translations/hr.json similarity index 71% rename from homeassistant/components/risco/translations/zh-Hans.json rename to homeassistant/components/ambient_station/translations/hr.json index a5f4ff11f09..b4c376c3855 100644 --- a/homeassistant/components/risco/translations/zh-Hans.json +++ b/homeassistant/components/ambient_station/translations/hr.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "username": "\u7528\u6237\u540d" + "api_key": "API klju\u010d" } } } diff --git a/homeassistant/components/ambient_station/translations/sk.json b/homeassistant/components/ambient_station/translations/sk.json index 01c13a4f11e..285f13630cc 100644 --- a/homeassistant/components/ambient_station/translations/sk.json +++ b/homeassistant/components/ambient_station/translations/sk.json @@ -1,13 +1,19 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, "error": { - "invalid_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + "invalid_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "no_devices": "V \u00fa\u010dte sa nena\u0161li \u017eiadne zariadenia" }, "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" - } + "api_key": "API k\u013e\u00fa\u010d", + "app_key": "Aplika\u010dn\u00fd k\u013e\u00fa\u010d" + }, + "title": "Vypl\u0148te svoje \u00fadaje" } } } diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index df742b320db..9162d7841d1 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -225,7 +225,7 @@ class AmcrestCam(Camera): # Amcrest cameras only support one snapshot command at a time. # Hence need to wait if a previous snapshot has not yet finished. # Also need to check that camera is online and turned on before each wait - # and before initiating shapshot. + # and before initiating snapshot. while self._snapshot_task: self._check_snapshot_ok() _LOGGER.debug("Waiting for previous snapshot from %s", self._name) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 5bb1836928c..2e53d9c03d5 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -113,8 +113,10 @@ class Analytics: if stored: self._data = stored - if self.supervisor: - supervisor_info = hassio.get_supervisor_info(self.hass) + if ( + self.supervisor + and (supervisor_info := hassio.get_supervisor_info(self.hass)) is not None + ): if not self.onboarded: # User have not configured analytics, get this setting from the supervisor if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get( diff --git a/homeassistant/components/android_ip_webcam/translations/sk.json b/homeassistant/components/android_ip_webcam/translations/sk.json new file mode 100644 index 00000000000..820da08a7b6 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index bdc067c4275..ea51ddedfdb 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -9,7 +9,11 @@ from typing import Any from androidtv import state_detection_rules_validator import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -168,22 +172,22 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(OptionsFlow): +class OptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle an option flow for Android TV.""" def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" - self.config_entry = config_entry + super().__init__(config_entry) - apps = config_entry.options.get(CONF_APPS, {}) - det_rules = config_entry.options.get(CONF_STATE_DETECTION_RULES, {}) - self._apps: dict[str, Any] = apps.copy() - self._state_det_rules: dict[str, Any] = det_rules.copy() + self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {}) + self._state_det_rules: dict[str, Any] = self.options.setdefault( + CONF_STATE_DETECTION_RULES, {} + ) self._conf_app_id: str | None = None self._conf_rule_id: str | None = None @@ -222,7 +226,7 @@ class OptionsFlowHandler(OptionsFlow): apps_list = {k: f"{v} ({k})" if v else k for k, v in self._apps.items()} apps = {APPS_NEW_ID: "Add new", **apps_list} rules = [RULES_NEW_ID] + list(self._state_det_rules) - options = self.config_entry.options + options = self.options data_schema = vol.Schema( { diff --git a/homeassistant/components/androidtv/translations/ru.json b/homeassistant/components/androidtv/translations/ru.json index 61eb431fbf4..4bb20ad0374 100644 --- a/homeassistant/components/androidtv/translations/ru.json +++ b/homeassistant/components/androidtv/translations/ru.json @@ -7,7 +7,7 @@ "error": { "adbkey_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 ADB \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "key_and_server": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043b\u044e\u0447 ADB \u0438\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 ADB.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/androidtv/translations/sk.json b/homeassistant/components/androidtv/translations/sk.json index 86bba63ac49..509a1b4d0e6 100644 --- a/homeassistant/components/androidtv/translations/sk.json +++ b/homeassistant/components/androidtv/translations/sk.json @@ -1,14 +1,51 @@ { "config": { "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "invalid_unique_id": "Nie je mo\u017en\u00e9 ur\u010di\u0165 platn\u00e9 jedine\u010dn\u00e9 ID zariadenia" + }, + "error": { + "adbkey_not_file": "S\u00fabor k\u013e\u00fa\u010da ADB sa nena\u0161iel", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "key_and_server": "Poskytnite iba k\u013e\u00fa\u010d ADB alebo server ADB", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { + "adb_server_ip": "IP adresa servera ADB (nechajte pr\u00e1zdnu, ak sa nepou\u017e\u00edva)", + "adb_server_port": "Port servera ADB", + "adbkey": "Cesta k s\u00faboru k\u013e\u00fa\u010da ADB (pre automatick\u00e9 vygenerovanie nechajte pr\u00e1zdne)", + "device_class": "Typ zariadenia", + "host": "Hostite\u013e", "port": "Port" } } } + }, + "options": { + "step": { + "apps": { + "data": { + "app_delete": "Za\u010diarknut\u00edm tejto mo\u017enosti odstr\u00e1nite t\u00fato aplik\u00e1ciu", + "app_id": "ID aplik\u00e1cie", + "app_name": "N\u00e1zov aplik\u00e1cie" + }, + "description": "Konfigurova\u0165 ID aplik\u00e1cie {app_id}", + "title": "Konfigur\u00e1cia aplik\u00e1ci\u00ed Android TV" + }, + "init": { + "data": { + "apps": "Konfigur\u00e1cia zoznamu aplik\u00e1ci\u00ed" + } + }, + "rules": { + "data": { + "rule_id": "ID aplik\u00e1cie", + "rule_values": "Zoznam pravidiel zis\u0165ovania stavu (pozri dokument\u00e1ciu)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ca.json b/homeassistant/components/anthemav/translations/ca.json index 20785a9e67d..723883d5c1a 100644 --- a/homeassistant/components/anthemav/translations/ca.json +++ b/homeassistant/components/anthemav/translations/ca.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "La configuraci\u00f3 d'Anthem A/V Receivers mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Anthem A/V Receivers del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "La configuraci\u00f3 YAML d'Anthem A/V Receivers est\u00e0 sent eliminada" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json index d751349b005..622384629fe 100644 --- a/homeassistant/components/anthemav/translations/de.json +++ b/homeassistant/components/anthemav/translations/de.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Die Konfiguration von Anthem A/V-Receivern mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Anthem A/V Receivers YAML Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die YAML-Konfiguration von Anthem A/V Receivers wird entfernt" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/el.json b/homeassistant/components/anthemav/translations/el.json index 5da08413782..983e89155e8 100644 --- a/homeassistant/components/anthemav/translations/el.json +++ b/homeassistant/components/anthemav/translations/el.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03c4\u03ce\u03bd Anthem A/V \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 Anthem A/V Receivers \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 Anthem A/V Receivers \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/es.json b/homeassistant/components/anthemav/translations/es.json index 7759dbd8607..f9609ad7c6b 100644 --- a/homeassistant/components/anthemav/translations/es.json +++ b/homeassistant/components/anthemav/translations/es.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de los Receptores A/V Anthem mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de los Receptores A/V Anthem de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la configuraci\u00f3n YAML de los Receptores A/V Anthem" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/et.json b/homeassistant/components/anthemav/translations/et.json index d5b9d4f224f..4ec356c8902 100644 --- a/homeassistant/components/anthemav/translations/et.json +++ b/homeassistant/components/anthemav/translations/et.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Anthem A/V-vastuv\u00f5tjate konfigureerimine YAML-i abil eemaldatakse. \n\n Teie olemasolev YAML-i konfiguratsioon imporditi kasutajaliidesesse automaatselt. \n\n Selle probleemi lahendamiseks eemaldage failist configuration.yaml konfiguratsioon Anthem A/V Receivers YAML ja taask\u00e4ivitage Home Assistant.", - "title": "Anthem A/V-vastuv\u00f5tjate YAML-konfiguratsioon eemaldatakse" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json index d08ef70d6b7..faf417552ce 100644 --- a/homeassistant/components/anthemav/translations/fr.json +++ b/homeassistant/components/anthemav/translations/fr.json @@ -15,10 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "title": "La configuration YAML pour les r\u00e9cepteurs A/V Anthem sera bient\u00f4t supprim\u00e9e" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/hu.json b/homeassistant/components/anthemav/translations/hu.json index af7d008356c..f13544fff61 100644 --- a/homeassistant/components/anthemav/translations/hu.json +++ b/homeassistant/components/anthemav/translations/hu.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Az Anthem A/V egys\u00e9gek YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el az Anthem A/V egys\u00e9gek YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "Az Anthem A/V Receivers YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json index 99843443ab9..1eb2ba0b5a1 100644 --- a/homeassistant/components/anthemav/translations/id.json +++ b/homeassistant/components/anthemav/translations/id.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Proses konfigurasi Integrasi Receiver Anthem A/V lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Receiver Anthem A/V dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Integrasi Anthem A/V Receiver dalam proses penghapusan" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json index 847332b57ad..12b0df56f0f 100644 --- a/homeassistant/components/anthemav/translations/it.json +++ b/homeassistant/components/anthemav/translations/it.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "La configurazione di Anthem A/V Receivers tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Anthem A/V Receivers sar\u00e0 rimossa" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json index 9a349743cf5..8c87d02e557 100644 --- a/homeassistant/components/anthemav/translations/ja.json +++ b/homeassistant/components/anthemav/translations/ja.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Anthem A/V Receivers\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Anthem A/V Receivers\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "title": "Anthem A/V Receivers YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/nl.json b/homeassistant/components/anthemav/translations/nl.json index 8754427ea61..c09dde1bfc3 100644 --- a/homeassistant/components/anthemav/translations/nl.json +++ b/homeassistant/components/anthemav/translations/nl.json @@ -14,10 +14,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "title": "De Anthem A/V Receivers YAML-configuratie wordt verwijderd" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/no.json b/homeassistant/components/anthemav/translations/no.json index ae6b59cc89c..e7b3f66ae8d 100644 --- a/homeassistant/components/anthemav/translations/no.json +++ b/homeassistant/components/anthemav/translations/no.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Anthem A/V-mottakere ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Anthem A/V Receivers YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "Anthem A/V-mottakernes YAML-konfigurasjon blir fjernet" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pl.json b/homeassistant/components/anthemav/translations/pl.json index ca40384e6f0..18eaecb9845 100644 --- a/homeassistant/components/anthemav/translations/pl.json +++ b/homeassistant/components/anthemav/translations/pl.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfiguracja Anthem A/V Receivers przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Konfiguracja YAML dla Anthem A/V Receivers zostanie usuni\u0119ta" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json index dbfe4b35801..096d41e214f 100644 --- a/homeassistant/components/anthemav/translations/pt-BR.json +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o de receptores A/V Anthem usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML dos receptores A/V do Anthem do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o YAML dos receptores A/V do Anthem est\u00e1 sendo removida" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json index f56475d331d..0f343609e4c 100644 --- a/homeassistant/components/anthemav/translations/ru.json +++ b/homeassistant/components/anthemav/translations/ru.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/sk.json b/homeassistant/components/anthemav/translations/sk.json new file mode 100644 index 00000000000..efae6c31512 --- /dev/null +++ b/homeassistant/components/anthemav/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "cannot_receive_deviceinfo": "Nepodarilo sa na\u010d\u00edta\u0165 adresu MAC. Uistite sa, \u017ee je zariadenie zapnut\u00e9" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/sv.json b/homeassistant/components/anthemav/translations/sv.json index dd3f6f891e2..cdb8ea7b9fe 100644 --- a/homeassistant/components/anthemav/translations/sv.json +++ b/homeassistant/components/anthemav/translations/sv.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Anthem A/V-mottagare med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Anthem A/V Receivers YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Anthem A/V-mottagarens YAML-konfiguration tas bort" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/tr.json b/homeassistant/components/anthemav/translations/tr.json index c77f5a1f14a..cbe85a5319c 100644 --- a/homeassistant/components/anthemav/translations/tr.json +++ b/homeassistant/components/anthemav/translations/tr.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Anthem A/V Al\u0131c\u0131lar\u0131n\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131.\n\nAnthem A/V Al\u0131c\u0131lar\u0131 YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "Anthem A/V Al\u0131c\u0131lar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" - } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index d68bd690c48..d1b286afd81 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -15,11 +15,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" - } } } \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/bg.json b/homeassistant/components/apcupsd/translations/bg.json index 0160e0fee55..da0aaf6631c 100644 --- a/homeassistant/components/apcupsd/translations/bg.json +++ b/homeassistant/components/apcupsd/translations/bg.json @@ -17,6 +17,7 @@ }, "issues": { "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 APC UPS Daemon \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 APC UPS Daemon \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 APC UPS Daemon \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" } } diff --git a/homeassistant/components/apcupsd/translations/cs.json b/homeassistant/components/apcupsd/translations/cs.json new file mode 100644 index 00000000000..5d403348397 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/sk.json b/homeassistant/components/apcupsd/translations/sk.json new file mode 100644 index 00000000000..c441c50a3a9 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_status": "Z Hostite\u013e nie je hl\u00e1sen\u00fd \u017eiadny stav" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index b78add3260e..d000c0346af 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -22,6 +22,10 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from homeassistant.util.network import is_ipv6_address from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN @@ -36,6 +40,15 @@ DEFAULT_START_OFF = False DISCOVERY_AGGREGATION_TIME = 15 # seconds +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_START_OFF, default=DEFAULT_START_OFF): bool, + } +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + async def device_scan(hass, identifier, loop): """Scan for a specific device using identifier as filter.""" @@ -76,9 +89,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> AppleTVOptionsFlow: + ) -> SchemaOptionsFlowHandler: """Get options flow for this handler.""" - return AppleTVOptionsFlow(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) def __init__(self): """Initialize a new AppleTVConfigFlow.""" @@ -525,35 +538,6 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self.atv.name, data=data) -class AppleTVOptionsFlow(config_entries.OptionsFlow): - """Handle Apple TV options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize Apple TV options flow.""" - self.config_entry = config_entry - self.options = dict(config_entry.options) - - async def async_step_init(self, user_input=None): - """Manage the Apple TV options.""" - if user_input is not None: - self.options[CONF_START_OFF] = user_input[CONF_START_OFF] - return self.async_create_entry(title="", data=self.options) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_START_OFF, - default=self.config_entry.options.get( - CONF_START_OFF, DEFAULT_START_OFF - ), - ): bool, - } - ), - ) - - class DeviceNotFound(HomeAssistantError): """Error to indicate device could not be found.""" diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json index cd0488141c4..0417c28d219 100644 --- a/homeassistant/components/apple_tv/translations/bg.json +++ b/homeassistant/components/apple_tv/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "ipv6_not_supported": "IPv6 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430.", "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json index 27ecc3452f0..1a4155977ad 100644 --- a/homeassistant/components/apple_tv/translations/de.json +++ b/homeassistant/components/apple_tv/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "backoff": "Das Ger\u00e4t akzeptiert zur Zeit keine Kopplungsanfragen (Du hast m\u00f6glicherweise zu oft einen ung\u00fcltigen PIN-Code eingegeben), versuche es sp\u00e4ter erneut.", + "backoff": "Das Ger\u00e4t akzeptiert zurzeit keine Kopplungsanfragen (Du hast m\u00f6glicherweise zu oft einen ung\u00fcltigen PIN-Code eingegeben), versuche es sp\u00e4ter erneut.", "device_did_not_pair": "Es wurde kein Versuch unternommen, den Kopplungsvorgang vom Ger\u00e4t aus abzuschlie\u00dfen.", "device_not_found": "Das Ger\u00e4t wurde bei der Erkennung nicht gefunden. Bitte versuche es erneut hinzuzuf\u00fcgen.", "inconsistent_device": "Die erwarteten Protokolle wurden bei der Erkennung nicht gefunden. Dies deutet normalerweise auf ein Problem mit Multicast-DNS (Zeroconf) hin. Bitte versuche das Ger\u00e4t erneut hinzuzuf\u00fcgen.", @@ -41,7 +41,7 @@ "title": "Passwort erforderlich" }, "protocol_disabled": { - "description": "Die Kopplung ist f\u00fcr `{protocol}` erforderlich, aber auf dem Ger\u00e4t deaktiviert. Bitte \u00fcberpr\u00fcfe m\u00f6gliche Zugriffsbeschr\u00e4nkungen (z. B. Verbindung aller Ger\u00e4te im lokalen Netzwerk zulassen) auf dem Ger\u00e4t. \n\nDu kannst fortfahren, ohne dieses Protokoll zu koppeln, aber einige Funktionen sind eingeschr\u00e4nkt.", + "description": "Die Kopplung ist f\u00fcr `{protocol}` erforderlich, aber auf dem Ger\u00e4t deaktiviert. Bitte \u00fcberpr\u00fcfe m\u00f6gliche Zugriffsbeschr\u00e4nkungen (z.B. Verbindung aller Ger\u00e4te im lokalen Netzwerk zulassen) auf dem Ger\u00e4t. \n\nDu kannst fortfahren, ohne dieses Protokoll zu koppeln, aber einige Funktionen sind eingeschr\u00e4nkt.", "title": "Kopplung nicht m\u00f6glich" }, "reconfigure": { diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 26ec85e8dc7..3af73091d3e 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -4,14 +4,14 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "ipv6_not_supported": "IPv6 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da.", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name} ({type})", diff --git a/homeassistant/components/apple_tv/translations/sk.json b/homeassistant/components/apple_tv/translations/sk.json index e0e6b1c5bda..18961885448 100644 --- a/homeassistant/components/apple_tv/translations/sk.json +++ b/homeassistant/components/apple_tv/translations/sk.json @@ -1,11 +1,66 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "ipv6_not_supported": "IPv6 nie je podporovan\u00e9", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "setup_failed": "Zariadenie sa nepodarilo nastavi\u0165.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "invalid_auth": "Neplatn\u00e9 overenie", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({type})", + "step": { + "confirm": { + "title": "Potvr\u010fte pridanie Apple TV" + }, + "pair_no_pin": { + "description": "Pre slu\u017ebu `{protocol}` sa vy\u017eaduje p\u00e1rovanie. Ak chcete pokra\u010dova\u0165, zadajte na svojom zariaden\u00ed k\u00f3d PIN {pin}.", + "title": "P\u00e1rovanie" + }, + "pair_with_pin": { + "data": { + "pin": "PIN k\u00f3d" + }, + "title": "P\u00e1rovanie" + }, + "password": { + "description": "`{protocol}` vy\u017eaduje heslo. Toto zatia\u013e nie je podporovan\u00e9. Ak chcete pokra\u010dova\u0165, deaktivujte heslo.", + "title": "Vy\u017eaduje sa heslo" + }, + "protocol_disabled": { + "title": "P\u00e1rovanie nie je mo\u017en\u00e9" + }, + "reconfigure": { + "description": "Prekonfigurujte toto zariadenie, aby ste obnovili jeho funk\u010dnos\u0165.", + "title": "Rekonfigur\u00e1cia zariadenia" + }, + "service_problem": { + "description": "Pri p\u00e1rovan\u00ed protokolu `{protocol}` do\u0161lo k probl\u00e9mu. Bude sa ignorova\u0165.", + "title": "Slu\u017ebu sa nepodarilo prida\u0165" + }, + "user": { + "data": { + "device_input": "Zariadenie" + }, + "title": "Nastavte nov\u00fa Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Nezap\u00ednajte zariadenie pri sp\u00fa\u0161\u0165an\u00ed Home Assistant" + }, + "description": "Nakonfigurujte v\u0161eobecn\u00e9 nastavenia zariadenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index c0dc9ab4497..984ecea50d7 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==1.1.0"], + "requirements": ["apprise==1.2.0"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index 6979eab4516..d12af3e6c7e 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -3,7 +3,7 @@ "name": "APRS", "documentation": "https://www.home-assistant.io/integrations/aprs", "codeowners": ["@PhilRW"], - "requirements": ["aprslib==0.7.0", "geopy==2.1.0"], + "requirements": ["aprslib==0.7.0", "geopy==2.3.0"], "iot_class": "cloud_push", "loggers": ["aprslib", "geographiclib", "geopy"] } diff --git a/homeassistant/components/aranet/__init__.py b/homeassistant/components/aranet/__init__.py new file mode 100644 index 00000000000..07e19ca2618 --- /dev/null +++ b/homeassistant/components/aranet/__init__.py @@ -0,0 +1,56 @@ +"""The Aranet integration.""" +from __future__ import annotations + +import logging + +from aranet4.client import Aranet4Advertisement + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +def _service_info_to_adv( + service_info: BluetoothServiceInfoBleak, +) -> Aranet4Advertisement: + return Aranet4Advertisement(service_info.device, service_info.advertisement) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Aranet from a config entry.""" + + address = entry.unique_id + assert address is not None + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=_service_info_to_adv, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/aranet/config_flow.py b/homeassistant/components/aranet/config_flow.py new file mode 100644 index 00000000000..029ee251ae7 --- /dev/null +++ b/homeassistant/components/aranet/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow for Aranet integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aranet4.client import Aranet4Advertisement, Version as AranetVersion +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import AbortFlow, FlowResult + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +MIN_VERSION = AranetVersion(1, 2, 0) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aranet.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up a new config flow for Aranet.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: Aranet4Advertisement | None = None + self._discovered_devices: dict[str, tuple[str, Aranet4Advertisement]] = {} + + def _raise_for_advertisement_errors(self, adv: Aranet4Advertisement) -> None: + """Raise any configuration errors that apply to an advertisement.""" + # Old versions of firmware don't expose sensor data in advertisements. + if not adv.manufacturer_data or adv.manufacturer_data.version < MIN_VERSION: + raise AbortFlow("outdated_version") + + # If integrations are disabled, we get no sensor data. + if not adv.manufacturer_data.integrations: + raise AbortFlow("integrations_disabled") + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the Bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + adv = Aranet4Advertisement(discovery_info.device, discovery_info.advertisement) + self._raise_for_advertisement_errors(adv) + + self._discovery_info = discovery_info + self._discovered_device = adv + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + adv = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = adv.readings.name if adv.readings else discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + adv = self._discovered_devices[address][1] + self._raise_for_advertisement_errors(adv) + + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address][0], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + + adv = Aranet4Advertisement( + discovery_info.device, discovery_info.advertisement + ) + if adv.manufacturer_data: + self._discovered_devices[address] = ( + adv.readings.name if adv.readings else discovery_info.name, + adv, + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + addr: dev[0] + for (addr, dev) in self._discovered_devices.items() + } + ) + } + ), + ) diff --git a/homeassistant/components/aranet/const.py b/homeassistant/components/aranet/const.py new file mode 100644 index 00000000000..056c627daa8 --- /dev/null +++ b/homeassistant/components/aranet/const.py @@ -0,0 +1,3 @@ +"""Constants for the Aranet integration.""" + +DOMAIN = "aranet" diff --git a/homeassistant/components/aranet/manifest.json b/homeassistant/components/aranet/manifest.json new file mode 100644 index 00000000000..6dc5cbe903c --- /dev/null +++ b/homeassistant/components/aranet/manifest.json @@ -0,0 +1,23 @@ +{ + "domain": "aranet", + "name": "Aranet", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aranet", + "requirements": ["aranet4==2.1.3"], + "dependencies": ["bluetooth"], + "codeowners": ["@aschmitz"], + "iot_class": "local_push", + "integration_type": "device", + "bluetooth": [ + { + "manufacturer_id": 1794, + "service_uuid": "f0cd1400-95da-4f4b-9ac8-aa55d312af0c", + "connectable": false + }, + { + "manufacturer_id": 1794, + "service_uuid": "0000fce0-0000-1000-8000-00805f9b34fb", + "connectable": false + } + ] +} diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py new file mode 100644 index 00000000000..6d8c7feb0ac --- /dev/null +++ b/homeassistant/components/aranet/sensor.py @@ -0,0 +1,169 @@ +"""Support for Aranet sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from aranet4.client import Aranet4Advertisement +from bleak.backends.device import BLEDevice + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_NAME, + ATTR_SW_VERSION, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + PRESSURE_HPA, + TEMP_CELSIUS, + TIME_SECONDS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + "temperature": SensorEntityDescription( + key="temperature", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "humidity": SensorEntityDescription( + key="humidity", + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "pressure": SensorEntityDescription( + key="pressure", + name="Pressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + "co2": SensorEntityDescription( + key="co2", + name="Carbon Dioxide", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + "battery": SensorEntityDescription( + key="battery", + name="Battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "interval": SensorEntityDescription( + key="update_interval", + name="Update Interval", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_SECONDS, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device: BLEDevice, + key: str, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(key, device.address) + + +def _sensor_device_info_to_hass( + adv: Aranet4Advertisement, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if adv.readings and adv.readings.name: + hass_device_info[ATTR_NAME] = adv.readings.name + if adv.manufacturer_data: + hass_device_info[ATTR_SW_VERSION] = str(adv.manufacturer_data.version) + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + adv: Aranet4Advertisement, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a Bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={adv.device.address: _sensor_device_info_to_hass(adv)}, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(adv.device, key): desc + for key, desc in SENSOR_DESCRIPTIONS.items() + }, + entity_data={ + _device_key_to_bluetooth_entity_key(adv.device, key): getattr( + adv.readings, key, None + ) + for key in SENSOR_DESCRIPTIONS + }, + entity_names={ + _device_key_to_bluetooth_entity_key(adv.device, key): desc.name + for key, desc in SENSOR_DESCRIPTIONS.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Aranet sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + Aranet4BluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class Aranet4BluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of an Aranet sensor.""" + + @property + def available(self) -> bool: + """Return whether the entity was available in the last update.""" + # Our superclass covers "did the device disappear entirely", but if the + # device has smart home integrations disabled, it will send BLE beacons + # without data, which we turn into Nones here. Because None is never a + # valid value for any of the Aranet sensors, that means the entity is + # actually unavailable. + return ( + super().available + and self.processor.entity_data.get(self.entity_key) is not None + ) + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/aranet/strings.json b/homeassistant/components/aranet/strings.json new file mode 100644 index 00000000000..1970beec210 --- /dev/null +++ b/homeassistant/components/aranet/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "integrations_diabled": "This device doesn't have integrations enabled. Please enable smart home integrations using the app and try again.", + "no_devices_found": "No unconfigured Aranet devices found.", + "outdated_version": "This device is using outdated firmware. Please update it to at least v1.2.0 and try again." + } + } +} diff --git a/homeassistant/components/aranet/translations/bg.json b/homeassistant/components/aranet/translations/bg.json new file mode 100644 index 00000000000..19937c000c8 --- /dev/null +++ b/homeassistant/components/aranet/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 Aranet \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "outdated_version": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0441\u0442\u0430\u0440\u044f\u043b \u0444\u044a\u0440\u043c\u0443\u0435\u0440. \u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0433\u043e \u043f\u043e\u043d\u0435 \u0434\u043e v1.2.0 \u0438 \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/ca.json b/homeassistant/components/aranet/translations/ca.json new file mode 100644 index 00000000000..ceaa1750762 --- /dev/null +++ b/homeassistant/components/aranet/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "integrations_diabled": "Aquest dispositiu no t\u00e9 les integracions activades. Activa les integracions de llar intel\u00b7ligent a trav\u00e9s de l'aplicaci\u00f3 i torna-ho a provar.", + "no_devices_found": "No s'han trobat dispositius Aranet no configurats.", + "outdated_version": "El dispositiu est\u00e0 utilitzant programari obsolet. Actualitza'l com a m\u00ednim a la versi\u00f3 1.2.0 i torna-ho a provar." + }, + "error": { + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/de.json b/homeassistant/components/aranet/translations/de.json new file mode 100644 index 00000000000..b9b37b32e22 --- /dev/null +++ b/homeassistant/components/aranet/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "integrations_diabled": "Auf diesem Ger\u00e4t sind keine Integrationen aktiviert. Bitte aktiviere die Smart-Home-Integration \u00fcber die App und versuche es erneut.", + "no_devices_found": "Keine unkonfigurierten Aranet Ger\u00e4te gefunden.", + "outdated_version": "Dieses Ger\u00e4t verwendet eine veraltete Firmware. Bitte aktualisiere es auf mindestens v1.2.0 und versuche es erneut." + }, + "error": { + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/el.json b/homeassistant/components/aranet/translations/el.json new file mode 100644 index 00000000000..9a667f53fbf --- /dev/null +++ b/homeassistant/components/aranet/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "integrations_diabled": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03ce\u03c3\u03b5\u03b9\u03c2. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03ce\u03c3\u03b5\u03b9\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03bf\u03c5 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Aranet.", + "outdated_version": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c0\u03b1\u03bb\u03b9\u03cc \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03c3\u03b5 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 1.2.0 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + }, + "error": { + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/en.json b/homeassistant/components/aranet/translations/en.json new file mode 100644 index 00000000000..00d5aacf11c --- /dev/null +++ b/homeassistant/components/aranet/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "integrations_diabled": "This device doesn't have integrations enabled. Please enable smart home integrations using the app and try again.", + "no_devices_found": "No unconfigured Aranet devices found.", + "outdated_version": "This device is using outdated firmware. Please update it to at least v1.2.0 and try again." + }, + "error": { + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/es.json b/homeassistant/components/aranet/translations/es.json new file mode 100644 index 00000000000..c2e96ecfc7a --- /dev/null +++ b/homeassistant/components/aranet/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "integrations_diabled": "Este dispositivo no tiene integraciones habilitadas. Por favor, habilita las integraciones de hogares inteligentes usando la aplicaci\u00f3n y int\u00e9ntalo de nuevo.", + "no_devices_found": "No se han encontrado dispositivos Aranet no configurados.", + "outdated_version": "Este dispositivo est\u00e1 utilizando firmware obsoleto. Por favor, actual\u00edzalo al menos a v1.2.0 e int\u00e9ntalo de nuevo." + }, + "error": { + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/et.json b/homeassistant/components/aranet/translations/et.json new file mode 100644 index 00000000000..989add44454 --- /dev/null +++ b/homeassistant/components/aranet/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "integrations_diabled": "Sellel seadmel pole sidumised lubatud. Luba rakenduse abil nutika kodu sidumine ja proovi uuesti.", + "no_devices_found": "H\u00e4\u00e4lestamata Araneti seadmeid ei leitud.", + "outdated_version": "See seade kasutab aegunud p\u00fcsivara. V\u00e4rskenda see v\u00e4hemalt versioonile 1.2.0 ja proovi uuesti." + }, + "error": { + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/fr.json b/homeassistant/components/aranet/translations/fr.json new file mode 100644 index 00000000000..a21fd193d8b --- /dev/null +++ b/homeassistant/components/aranet/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil Aranet non configur\u00e9 n\u2019a \u00e9t\u00e9 trouv\u00e9." + }, + "error": { + "unknown": "Erreur inattendue" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/he.json b/homeassistant/components/aranet/translations/he.json new file mode 100644 index 00000000000..b4afd666d40 --- /dev/null +++ b/homeassistant/components/aranet/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/hr.json b/homeassistant/components/aranet/translations/hr.json new file mode 100644 index 00000000000..ccdbb4f1906 --- /dev/null +++ b/homeassistant/components/aranet/translations/hr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran" + }, + "error": { + "unknown": "Neo\u010dekivana gre\u0161ka" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u017delite li postaviti {name}?" + }, + "user": { + "data": { + "address": "Ure\u0111aj" + }, + "description": "Odaberite ure\u0111aj za postavljanje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/hu.json b/homeassistant/components/aranet/translations/hu.json new file mode 100644 index 00000000000..2773a11e3dc --- /dev/null +++ b/homeassistant/components/aranet/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "integrations_diabled": "Ezen az eszk\u00f6z\u00f6n nincs enged\u00e9lyezve az integr\u00e1ci\u00f3. K\u00e9rj\u00fck, enged\u00e9lyezze az okosotthon-integr\u00e1ci\u00f3kat az alkalmaz\u00e1s seg\u00edts\u00e9g\u00e9vel, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan Aranet eszk\u00f6z.", + "outdated_version": "Ez az eszk\u00f6z elavult firmware-t haszn\u00e1l. K\u00e9rj\u00fck, friss\u00edtse legal\u00e1bb 1.2.0-s verzi\u00f3ra, \u00e9s pr\u00f3b\u00e1lja \u00fajra." + }, + "error": { + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/id.json b/homeassistant/components/aranet/translations/id.json new file mode 100644 index 00000000000..c41c46f5138 --- /dev/null +++ b/homeassistant/components/aranet/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "integrations_diabled": "Perangkat ini tidak memiliki integrasi yang diaktifkan. Aktifkan integrasi rumah cerdas menggunakan aplikasi dan coba lagi.", + "no_devices_found": "Tidak ditemukan perangkat Aranet yang tidak dikonfigurasi.", + "outdated_version": "Perangkat ini menggunakan firmware usang. Perbarui setidaknya ke firmware v1.2.0 dan coba lagi." + }, + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/it.json b/homeassistant/components/aranet/translations/it.json new file mode 100644 index 00000000000..372e6266b5b --- /dev/null +++ b/homeassistant/components/aranet/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "integrations_diabled": "Questo dispositivo non ha integrazioni abilitate. Si prega di abilitare le integrazioni di smart home utilizzando l'app e riprovare.", + "no_devices_found": "Non sono stati trovati dispositivi Aranet non configurati.", + "outdated_version": "Questo dispositivo utilizza un firmware obsoleto. Aggiornalo almeno alla v1.2.0 e riprova." + }, + "error": { + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/nl.json b/homeassistant/components/aranet/translations/nl.json new file mode 100644 index 00000000000..64f3286888c --- /dev/null +++ b/homeassistant/components/aranet/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/no.json b/homeassistant/components/aranet/translations/no.json new file mode 100644 index 00000000000..8e4a732972a --- /dev/null +++ b/homeassistant/components/aranet/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "integrations_diabled": "Denne enheten har ikke integrasjoner aktivert. Aktiver smarthus-integrasjoner ved hjelp av appen og pr\u00f8v igjen.", + "no_devices_found": "Fant ingen ukonfigurerte Aranet-enheter.", + "outdated_version": "Denne enheten bruker utdatert fastvare. Oppdater den til minst v1.2.0 og pr\u00f8v igjen." + }, + "error": { + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/pl.json b/homeassistant/components/aranet/translations/pl.json new file mode 100644 index 00000000000..3737a66471e --- /dev/null +++ b/homeassistant/components/aranet/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "integrations_diabled": "To urz\u0105dzenie nie ma w\u0142\u0105czonej integracji. W\u0142\u0105cz integracj\u0119 z inteligentnym domem za pomoc\u0105 aplikacji i spr\u00f3buj ponownie.", + "no_devices_found": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144 Aranet.", + "outdated_version": "To urz\u0105dzenie korzysta z przestarza\u0142ego oprogramowania. Zaktualizuj go do wersji co najmniej 1.2.0 i spr\u00f3buj ponownie." + }, + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/pt-BR.json b/homeassistant/components/aranet/translations/pt-BR.json new file mode 100644 index 00000000000..d286add568d --- /dev/null +++ b/homeassistant/components/aranet/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "integrations_diabled": "Este dispositivo n\u00e3o tem integra\u00e7\u00f5es ativadas. Ative as integra\u00e7\u00f5es de casa inteligente usando o aplicativo e tente novamente.", + "no_devices_found": "Nenhum dispositivo Aranet n\u00e3o configurado encontrado.", + "outdated_version": "Este dispositivo est\u00e1 usando um firmware desatualizado. Atualize-o para pelo menos a v1.2.0 e tente novamente." + }, + "error": { + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/ru.json b/homeassistant/components/aranet/translations/ru.json new file mode 100644 index 00000000000..11477454e63 --- /dev/null +++ b/homeassistant/components/aranet/translations/ru.json @@ -0,0 +1,25 @@ +{ + "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.", + "integrations_diabled": "\u041d\u0430 \u044d\u0442\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f. \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0443\u043c\u043d\u043e\u0433\u043e \u0434\u043e\u043c\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "no_devices_found": "\u041d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e.", + "outdated_version": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0443\u044e \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0435\u0451 \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 1.2.0 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." + }, + "error": { + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/sk.json b/homeassistant/components/aranet/translations/sk.json new file mode 100644 index 00000000000..83276dd71fd --- /dev/null +++ b/homeassistant/components/aranet/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "outdated_version": "Toto zariadenie pou\u017e\u00edva zastaran\u00fd firmv\u00e9r. Aktualizujte ho aspo\u0148 na verziu 1.2.0 a sk\u00faste to znova." + }, + "error": { + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/zh-Hant.json b/homeassistant/components/aranet/translations/zh-Hant.json new file mode 100644 index 00000000000..586c871e043 --- /dev/null +++ b/homeassistant/components/aranet/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "integrations_diabled": "\u88dd\u7f6e\u4e26\u672a\u555f\u7528\u4efb\u4f55\u6574\u5408\uff0c\u8acb\u5148\u4f7f\u7528 App \u555f\u7528\u667a\u80fd\u5bb6\u5ead\u6574\u5408\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "no_devices_found": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a Aranet \u88dd\u7f6e\u3002", + "outdated_version": "\u88dd\u7f6e\u4f7f\u7528\u4e86\u904e\u820a\u7684\u97cc\u9ad4\uff0c\u8acb\u66f4\u65b0\u81f3 v1.2.0 \u7248\u4ee5\u4e0a\u4e26\u518d\u8a66\u4e00\u6b21\u3002" + }, + "error": { + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index c2c6be0db30..2796a607ac0 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress import logging +from typing import Any from arcam.fmj import ConnectionFailed from arcam.fmj.client import Client @@ -31,7 +32,7 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] -async def _await_cancel(task): +async def _await_cancel(task: asyncio.Task) -> None: task.cancel() with suppress(asyncio.CancelledError): await task @@ -42,7 +43,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN_DATA_ENTRIES] = {} hass.data[DOMAIN_DATA_TASKS] = {} - async def _stop(_): + async def _stop(_: Any) -> None: asyncio.gather( *(_await_cancel(task) for task in hass.data[DOMAIN_DATA_TASKS].values()) ) @@ -80,8 +81,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _run_client(hass, client, interval): - def _listen(_): +async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None: + def _listen(_: Any) -> None: async_dispatcher_send(hass, SIGNAL_CLIENT_DATA, client.host) while True: diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py index 2570fd1aea5..09944328c4a 100644 --- a/homeassistant/components/arcam_fmj/config_flow.py +++ b/homeassistant/components/arcam_fmj/config_flow.py @@ -1,4 +1,7 @@ """Config flow to configure the Arcam FMJ component.""" +from __future__ import annotations + +from typing import Any from urllib.parse import urlparse from arcam.fmj.client import Client, ConnectionFailed @@ -8,15 +11,17 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES -def get_entry_client(hass, entry): +def get_entry_client(hass: HomeAssistant, entry: config_entries.ConfigEntry) -> Client: """Retrieve client associated with a config entry.""" - return hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] + client: Client = hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] + return client class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -24,11 +29,13 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _async_set_unique_id_and_update(self, host, port, uuid): + async def _async_set_unique_id_and_update( + self, host: str, port: int, uuid: str + ) -> None: await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port}) - async def _async_check_and_create(self, host, port): + async def _async_check_and_create(self, host: str, port: int) -> FlowResult: client = Client(host, port) try: await client.start() @@ -42,9 +49,11 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_HOST: host, CONF_PORT: port}, ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a discovered device.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: uuid = await get_uniqueid_from_host( @@ -68,7 +77,9 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_confirm(self, user_input=None): + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle user-confirmation of discovered node.""" context = self.context placeholders = { @@ -87,9 +98,11 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered device.""" - host = urlparse(discovery_info.ssdp_location).hostname + host = str(urlparse(discovery_info.ssdp_location).hostname) port = DEFAULT_PORT uuid = get_uniqueid_from_udn(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) + if not uuid: + return self.async_abort(reason="cannot_connect") await self._async_set_unique_id_and_update(host, port, uuid) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 13f1acc7244..f3722c81ec5 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -65,7 +65,7 @@ async def async_attach_trigger( entity_id = config[CONF_ENTITY_ID] @callback - def _handle_event(event: Event): + def _handle_event(event: Event) -> None: if event.data[ATTR_ENTITY_ID] == entity_id: hass.async_run_hass_job( job, diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index c91c92922b4..3fcbaabc7e8 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,7 +3,7 @@ "name": "Arcam FMJ Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.12.0"], + "requirements": ["arcam-fmj==1.0.1"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 65a5d8c3580..08a65b71193 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -61,18 +61,17 @@ class ArcamFmj(MediaPlayerEntity): """Representation of a media device.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, - device_name, + device_name: str, state: State, uuid: str, - ): + ) -> None: """Initialize device.""" self._state = state - self._device_name = device_name - self._attr_name = f"{device_name} - Zone: {state.zn}" - self._uuid = uuid + self._attr_name = f"Zone {state.zn}" self._attr_supported_features = ( MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.PLAY_MEDIA @@ -87,6 +86,14 @@ class ArcamFmj(MediaPlayerEntity): self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE self._attr_unique_id = f"{uuid}-{state.zn}" self._attr_entity_registry_enabled_default = state.zn == 1 + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, uuid), + }, + manufacturer="Arcam", + model="Arcam FMJ AVR", + name=device_name, + ) @property def state(self) -> MediaPlayerState: @@ -95,36 +102,23 @@ class ArcamFmj(MediaPlayerEntity): return MediaPlayerState.ON return MediaPlayerState.OFF - @property - def device_info(self): - """Return a device description for device registry.""" - return DeviceInfo( - identifiers={ - (DOMAIN, self._uuid), - (DOMAIN, self._state.client.host, self._state.client.port), - }, - manufacturer="Arcam", - model="Arcam FMJ AVR", - name=self._device_name, - ) - async def async_added_to_hass(self) -> None: """Once registered, add listener for events.""" await self._state.start() await self._state.update() @callback - def _data(host): + def _data(host: str) -> None: if host == self._state.client.host: self.async_write_ha_state() @callback - def _started(host): + def _started(host: str) -> None: if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) @callback - def _stopped(host): + def _stopped(host: str) -> None: if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) @@ -249,40 +243,40 @@ class ArcamFmj(MediaPlayerEntity): return @property - def source(self): + def source(self) -> str | None: """Return the current input source.""" if (value := self._state.get_source()) is None: return None return value.name @property - def source_list(self): + def source_list(self) -> list[str]: """List of available input sources.""" return [x.name for x in self._state.get_source_list()] @property - def sound_mode(self): + def sound_mode(self) -> str | None: """Name of the current sound mode.""" if (value := self._state.get_decode_mode()) is None: return None return value.name @property - def sound_mode_list(self): + def sound_mode_list(self) -> list[str] | None: """List of available sound modes.""" if (values := self._state.get_decode_modes()) is None: return None return [x.name for x in values] @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" if (value := self._state.get_mute()) is None: return None return value @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of device.""" if (value := self._state.get_volume()) is None: return None @@ -301,7 +295,7 @@ class ArcamFmj(MediaPlayerEntity): return value @property - def media_content_id(self): + def media_content_id(self) -> str | None: """Content type of current playing media.""" source = self._state.get_source() if source in (SourceCodes.DAB, SourceCodes.FM): @@ -315,7 +309,7 @@ class ArcamFmj(MediaPlayerEntity): return value @property - def media_channel(self): + def media_channel(self) -> str | None: """Channel currently playing.""" source = self._state.get_source() if source == SourceCodes.DAB: @@ -327,7 +321,7 @@ class ArcamFmj(MediaPlayerEntity): return value @property - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" if self._state.get_source() == SourceCodes.DAB: value = self._state.get_dls_pdt() @@ -336,7 +330,7 @@ class ArcamFmj(MediaPlayerEntity): return value @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" if (source := self._state.get_source()) is None: return None diff --git a/homeassistant/components/arcam_fmj/translations/bg.json b/homeassistant/components/arcam_fmj/translations/bg.json index f24b5481b2c..60b4c65d0a3 100644 --- a/homeassistant/components/arcam_fmj/translations/bg.json +++ b/homeassistant/components/arcam_fmj/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "{host}", "step": { diff --git a/homeassistant/components/arcam_fmj/translations/sk.json b/homeassistant/components/arcam_fmj/translations/sk.json index b41d6edbd4b..ad75e5f9df3 100644 --- a/homeassistant/components/arcam_fmj/translations/sk.json +++ b/homeassistant/components/arcam_fmj/translations/sk.json @@ -1,14 +1,24 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165" }, + "flow_title": "{host}", "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" - } + }, + "description": "Zadajte n\u00e1zov hostite\u013ea alebo IP adresu zariadenia." } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} bola po\u017eiadan\u00e1 o zapnutie" + } } } \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/sk.json b/homeassistant/components/aseko_pool_live/translations/sk.json index 72b0304f1c3..97c939543c0 100644 --- a/homeassistant/components/aseko_pool_live/translations/sk.json +++ b/homeassistant/components/aseko_pool_live/translations/sk.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" } } } diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index 94843a4c07c..6b0056b14fa 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging import os import socket -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( CONF_HOST, CONF_MODE, @@ -26,6 +26,11 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import ( CONF_DNSMASQ, @@ -52,6 +57,35 @@ RESULT_UNKNOWN = "unknown" _LOGGER = logging.getLogger(__name__) +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional( + CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME.total_seconds() + ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)), + vol.Optional(CONF_TRACK_UNKNOWN, default=DEFAULT_TRACK_UNKNOWN): bool, + vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): str, + vol.Required(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): str, + } +) + + +async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Get options schema.""" + options_flow: SchemaOptionsFlowHandler + options_flow = cast(SchemaOptionsFlowHandler, handler.parent_handler) + if options_flow.config_entry.data[CONF_MODE] == MODE_AP: + return OPTIONS_SCHEMA.extend( + { + vol.Optional(CONF_REQUIRE_IP, default=True): bool, + } + ) + return OPTIONS_SCHEMA + + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(get_options_schema), +} + def _is_file(value: str) -> bool: """Validate that the value is an existing file.""" @@ -203,62 +237,8 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(OptionsFlow): - """Handle a option flow for AsusWrt.""" - - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - data_schema = vol.Schema( - { - vol.Optional( - CONF_CONSIDER_HOME, - default=self.config_entry.options.get( - CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() - ), - ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)), - vol.Optional( - CONF_TRACK_UNKNOWN, - default=self.config_entry.options.get( - CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN - ), - ): bool, - vol.Required( - CONF_INTERFACE, - default=self.config_entry.options.get( - CONF_INTERFACE, DEFAULT_INTERFACE - ), - ): str, - vol.Required( - CONF_DNSMASQ, - default=self.config_entry.options.get( - CONF_DNSMASQ, DEFAULT_DNSMASQ - ), - ): str, - } - ) - - if self.config_entry.data[CONF_MODE] == MODE_AP: - data_schema = data_schema.extend( - { - vol.Optional( - CONF_REQUIRE_IP, - default=self.config_entry.options.get(CONF_REQUIRE_IP, True), - ): bool, - } - ) - - return self.async_show_form(step_id="init", data_schema=data_schema) + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> SchemaOptionsFlowHandler: + """Get options flow for this handler.""" + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index efdf4993927..3a9abdd7e85 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,8 +1,7 @@ """Support for ASUSWRT routers.""" from __future__ import annotations -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/asuswrt/translations/cs.json b/homeassistant/components/asuswrt/translations/cs.json index 26358d8c4bd..883ace178af 100644 --- a/homeassistant/components/asuswrt/translations/cs.json +++ b/homeassistant/components/asuswrt/translations/cs.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa", + "ssh_not_file": "Soubor kl\u00ed\u010de SSH nebyl nalezen", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index 0253cd20d1b..770cff7eb39 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -6,7 +6,7 @@ }, "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_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "pwd_and_ssh": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", "pwd_or_ssh": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", "ssh_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", diff --git a/homeassistant/components/asuswrt/translations/sk.json b/homeassistant/components/asuswrt/translations/sk.json index 39d2e182c40..5e58d4bc638 100644 --- a/homeassistant/components/asuswrt/translations/sk.json +++ b/homeassistant/components/asuswrt/translations/sk.json @@ -1,10 +1,31 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "ssh_not_file": "S\u00fabor s k\u013e\u00fa\u010dom SSH nebol n\u00e1jden\u00fd", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", + "mode": "Re\u017eim", "name": "N\u00e1zov", - "port": "Port" + "password": "Heslo", + "port": "Port", + "protocol": "Komunika\u010dn\u00fd protokol, ktor\u00fd sa m\u00e1 pou\u017ei\u0165", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Nastavte po\u017eadovan\u00fd parameter na pripojenie k smerova\u010du" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "require_ip": "Zariadenia musia ma\u0165 IP (pre re\u017eim pr\u00edstupov\u00e9ho bodu)" } } } diff --git a/homeassistant/components/atag/translations/sk.json b/homeassistant/components/atag/translations/sk.json index 5b7f75998d7..14d096336a4 100644 --- a/homeassistant/components/atag/translations/sk.json +++ b/homeassistant/components/atag/translations/sk.json @@ -3,11 +3,16 @@ "abort": { "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" - } + }, + "title": "Pripojte sa k zariadeniu" } } } diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index 009f84a72ef..0b41373356d 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -30,7 +30,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): """Representation of an ATAG water heater.""" _attr_operation_list = OPERATION_LIST - _attr_supported_features = 0 _attr_temperature_unit = TEMP_CELSIUS @property diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index f4a0f57eb76..249d9e51a85 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -12,9 +12,9 @@ from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.lock import Lock, LockDetail from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub +from yalexs_ble import YaleXSBLEDiscovery -from homeassistant.components import yalexs_ble -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ( @@ -22,7 +22,7 @@ from homeassistant.exceptions import ( ConfigEntryNotReady, HomeAssistantError, ) -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from .activity import ActivityStream from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS @@ -38,6 +38,7 @@ API_CACHED_ATTRS = { "lock_status", "lock_status_datetime", } +YALEXS_BLE_DOMAIN = "yalexs_ble" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -100,9 +101,11 @@ def _async_trigger_ble_lock_discovery( ): """Update keys for the yalexs-ble integration if available.""" for lock_detail in locks_with_offline_keys: - yalexs_ble.async_discovery( + discovery_flow.async_create_flow( hass, - yalexs_ble.YaleXSBLEDiscovery( + YALEXS_BLE_DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data=YaleXSBLEDiscovery( { "name": lock_detail.device_name, "address": lock_detail.mac_address, diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index b7dde070049..e8f8736b522 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.6"], + "requirements": ["yalexs==1.2.6", "yalexs_ble==1.10.2"], "codeowners": ["@bdraco"], "dhcp": [ { @@ -24,6 +24,5 @@ ], "config_flow": true, "iot_class": "cloud_push", - "loggers": ["pubnub", "yalexs"], - "after_dependencies": ["yalexs_ble"] + "loggers": ["pubnub", "yalexs"] } diff --git a/homeassistant/components/august/translations/bg.json b/homeassistant/components/august/translations/bg.json index f2dccb231c1..110c7ee5598 100644 --- a/homeassistant/components/august/translations/bg.json +++ b/homeassistant/components/august/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { @@ -10,7 +11,7 @@ } }, "validation": { - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/august/translations/sk.json b/homeassistant/components/august/translations/sk.json index 71a7aea5018..ef420973986 100644 --- a/homeassistant/components/august/translations/sk.json +++ b/homeassistant/components/august/translations/sk.json @@ -1,10 +1,34 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_validate": { + "data": { + "password": "Heslo" + }, + "description": "Zadajte heslo pre {username}." + }, + "user_validate": { + "data": { + "login_method": "Sp\u00f4sob prihl\u00e1senia", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "validation": { + "data": { + "code": "Overovac\u00ed k\u00f3d" + }, + "title": "Dvojfaktorov\u00e1 autentifik\u00e1cia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py index a2331c19ec6..4649a3adc08 100644 --- a/homeassistant/components/aurora/config_flow.py +++ b/homeassistant/components/aurora/config_flow.py @@ -11,11 +11,26 @@ from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import CONF_THRESHOLD, DEFAULT_NAME, DEFAULT_THRESHOLD, DOMAIN _LOGGER = logging.getLogger(__name__) +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NOAA Aurora Integration.""" @@ -26,9 +41,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: + ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -81,34 +96,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle options flow changes.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Manage options.""" - - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Required( - CONF_THRESHOLD, - default=self.config_entry.options.get( - CONF_THRESHOLD, DEFAULT_THRESHOLD - ), - ): vol.All( - vol.Coerce(int), - vol.Range(min=0, max=100), - ), - } - ), - ) diff --git a/homeassistant/components/aurora/translations/sk.json b/homeassistant/components/aurora/translations/sk.json index 81532ef4801..ad49a4a2551 100644 --- a/homeassistant/components/aurora/translations/sk.json +++ b/homeassistant/components/aurora/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/sk.json b/homeassistant/components/aurora_abb_powerone/translations/sk.json new file mode 100644 index 00000000000..1163236043d --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_serial_ports": "Nena\u0161li sa \u017eiadne komunika\u010dn\u00e9 porty. Na komunik\u00e1ciu potrebujete platn\u00e9 zariadenie RS485." + }, + "error": { + "cannot_connect": "Nie je mo\u017en\u00e9 sa pripoji\u0165, skontrolujte s\u00e9riov\u00fd port, adresu, elektrick\u00e9 pripojenie a \u010di je meni\u010d zapnut\u00fd (na dennom svetle)", + "cannot_open_serial_port": "Ned\u00e1 sa otvori\u0165 s\u00e9riov\u00fd port, skontrolujte ho a sk\u00faste to znova", + "invalid_serial_port": "S\u00e9riov\u00fd port nie je platn\u00fdm zariaden\u00edm alebo ho nebolo mo\u017en\u00e9 otvori\u0165" + }, + "step": { + "user": { + "data": { + "address": "Adresa meni\u010da", + "port": "Port adapt\u00e9ra RS485 alebo USB-RS485" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/bg.json b/homeassistant/components/aussie_broadband/translations/bg.json index 8098935e86c..6771b472a59 100644 --- a/homeassistant/components/aussie_broadband/translations/bg.json +++ b/homeassistant/components/aussie_broadband/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "no_services_found": "\u041d\u0435 \u0431\u044f\u0445\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u043b\u0443\u0433\u0438 \u0437\u0430 \u0442\u043e\u0437\u0438 \u0430\u043a\u0430\u0443\u043d\u0442", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/aussie_broadband/translations/sk.json b/homeassistant/components/aussie_broadband/translations/sk.json index 2d028b5f36c..8db55654be4 100644 --- a/homeassistant/components/aussie_broadband/translations/sk.json +++ b/homeassistant/components/aussie_broadband/translations/sk.json @@ -1,23 +1,48 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "no_services_found": "Pre tento \u00fa\u010det sa nena\u0161li \u017eiadne slu\u017eby", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Aktualizova\u0165 heslo pre {username}", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "service": { + "data": { + "services": "Slu\u017eby" + }, "title": "Vyberte slu\u017eby" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } } } }, "options": { "abort": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "init": { + "data": { + "services": "Slu\u017eby" + }, "title": "Vyberte slu\u017eby" } } diff --git a/homeassistant/components/auth/translations/bg.json b/homeassistant/components/auth/translations/bg.json index d07e20a854c..4ccfc0ae79b 100644 --- a/homeassistant/components/auth/translations/bg.json +++ b/homeassistant/components/auth/translations/bg.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\u0417\u0430 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u0432\u043e-\u0431\u0430\u0437\u0438\u0440\u0430\u043d\u0438 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0438 \u043f\u0430\u0440\u043e\u043b\u0438, \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u0439\u0442\u0435 QR \u043a\u043e\u0434\u0430 \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0410\u043a\u043e \u043d\u044f\u043c\u0430\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0412\u0438 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 \u0438\u043b\u0438 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u0438\u043b\u0438 [Authy](https://authy.com/).\n\n{qr_code}\n\n\u0421\u043b\u0435\u0434 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u0430, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 6-\u0442\u0435 \u0446\u0438\u0444\u0440\u0438 \u043e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0437\u0430 \u0434\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0438\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435\u0442\u043e. \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 QR \u043a\u043e\u0434\u0430, \u043d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u043a\u043e\u0434 **`{code}`**.", + "description": "\u0417\u0430 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u0432\u043e-\u0431\u0430\u0437\u0438\u0440\u0430\u043d\u0438 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0438 \u043f\u0430\u0440\u043e\u043b\u0438, \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u0439\u0442\u0435 QR \u043a\u043e\u0434\u0430 \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0410\u043a\u043e \u043d\u044f\u043c\u0430\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0412\u0438 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u0438\u043b\u0438 [Authy](https://authy.com/).\n\n{qr_code}\n\n\u0421\u043b\u0435\u0434 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u0430, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 6-\u0442\u0435 \u0446\u0438\u0444\u0440\u0438 \u043e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0437\u0430 \u0434\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0438\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435\u0442\u043e. \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 QR \u043a\u043e\u0434\u0430, \u043d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u043a\u043e\u0434 **`{code}`**.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0447\u0440\u0435\u0437 TOTP" } }, diff --git a/homeassistant/components/auth/translations/de.json b/homeassistant/components/auth/translations/de.json index e33536bcc1c..ec97c449ac4 100644 --- a/homeassistant/components/auth/translations/de.json +++ b/homeassistant/components/auth/translations/de.json @@ -17,7 +17,7 @@ "title": "\u00dcberpr\u00fcfe das Setup" } }, - "title": "Benachrichtig f\u00fcr One-Time Password" + "title": "Benachrichtigen f\u00fcr One-Time Password" }, "totp": { "error": { diff --git a/homeassistant/components/auth/translations/hr.json b/homeassistant/components/auth/translations/hr.json new file mode 100644 index 00000000000..548b2bfdb4f --- /dev/null +++ b/homeassistant/components/auth/translations/hr.json @@ -0,0 +1,29 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nema dostupnih usluga obavijesti." + }, + "error": { + "invalid_code": "Kod nije valjan, poku\u0161ajte ponovo." + }, + "step": { + "init": { + "description": "Odaberite jednu od usluga obavijesti:", + "title": "Postavite jednokratnu lozinku koju isporu\u010duje komponenta obavijesti" + }, + "setup": { + "title": "Provjera postavki" + } + } + }, + "totp": { + "step": { + "init": { + "title": "Postavite dvofaktorsku autentifikaciju pomo\u0107u TOTP-a" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/sk.json b/homeassistant/components/auth/translations/sk.json new file mode 100644 index 00000000000..5d510baa4be --- /dev/null +++ b/homeassistant/components/auth/translations/sk.json @@ -0,0 +1,26 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nie s\u00fa k dispoz\u00edcii \u017eiadne notifika\u010dn\u00e9 slu\u017eby." + }, + "error": { + "invalid_code": "Neplatn\u00fd k\u00f3d, sk\u00faste to znova." + }, + "step": { + "init": { + "description": "Vyberte si jednu z notifika\u010dn\u00fdch slu\u017eieb:", + "title": "Nastavenie jednorazov\u00e9ho hesla doru\u010den\u00e9ho prostredn\u00edctvom komponentu notifik\u00e1cie" + }, + "setup": { + "description": "Jednorazov\u00e9 heslo bolo odoslan\u00e9 prostredn\u00edctvom **notify.{notify_service}**. Zadajte ho, pros\u00edm, ni\u017e\u0161ie:", + "title": "Overenie nastavenia" + } + }, + "title": "Ozn\u00e1menie jednorazov\u00e9ho hesla" + }, + "totp": { + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 234fcc97839..9581a6b1c40 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -817,9 +817,28 @@ async def _async_process_config( """ automation_matches: set[int] = set() config_matches: set[int] = set() + automation_configs_with_id: dict[str, tuple[int, AutomationEntityConfig]] = {} + automation_configs_without_id: list[tuple[int, AutomationEntityConfig]] = [] + + for config_idx, config in enumerate(automation_configs): + if automation_id := config.config_block.get(CONF_ID): + automation_configs_with_id[automation_id] = (config_idx, config) + continue + automation_configs_without_id.append((config_idx, config)) for automation_idx, automation in enumerate(automations): - for config_idx, config in enumerate(automation_configs): + if automation.unique_id: + if automation.unique_id not in automation_configs_with_id: + continue + config_idx, config = automation_configs_with_id.pop( + automation.unique_id + ) + if automation_matches_config(automation, config): + automation_matches.add(automation_idx) + config_matches.add(config_idx) + continue + + for config_idx, config in automation_configs_without_id: if config_idx in config_matches: # Only allow an automation config to match at most once continue diff --git a/homeassistant/components/automation/translations/sk.json b/homeassistant/components/automation/translations/sk.json index a300acd23da..d75fc6b3157 100644 --- a/homeassistant/components/automation/translations/sk.json +++ b/homeassistant/components/automation/translations/sk.json @@ -1,4 +1,16 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} pou\u017e\u00edva nezn\u00e1mu slu\u017ebu" + } + } + }, + "title": "{name} pou\u017e\u00edva nezn\u00e1mu slu\u017ebu" + } + }, "state": { "_": { "off": "Neakt\u00edvny", diff --git a/homeassistant/components/awair/translations/bg.json b/homeassistant/components/awair/translations/bg.json index 0f9884bf058..89e4326353a 100644 --- a/homeassistant/components/awair/translations/bg.json +++ b/homeassistant/components/awair/translations/bg.json @@ -1,11 +1,10 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_configured_account": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unreachable": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { @@ -23,9 +22,6 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {model} ({device_id})?" }, "local": { - "data": { - "host": "IP \u0430\u0434\u0440\u0435\u0441" - }, "description": "\u0421\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 [\u0442\u0435\u0437\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438]({url}) \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u043b\u043e\u043a\u0430\u043b\u043d\u0438\u044f API \u043d\u0430 Awair.\n\n\u0429\u0440\u0430\u043a\u043d\u0435\u0442\u0435 \u0432\u044a\u0440\u0445\u0443 \"\u0418\u0417\u041f\u0420\u0410\u0429\u0410\u041d\u0415\", \u043a\u043e\u0433\u0430\u0442\u043e \u0441\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u0438." }, "local_pick": { @@ -34,20 +30,12 @@ "host": "IP \u0430\u0434\u0440\u0435\u0441" } }, - "reauth": { - "data": { - "email": "\u0418\u043c\u0435\u0439\u043b" - } - }, "reauth_confirm": { "data": { "email": "\u0418\u043c\u0435\u0439\u043b" } }, "user": { - "data": { - "email": "\u0418\u043c\u0435\u0439\u043b" - }, "menu_options": { "cloud": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0447\u0440\u0435\u0437 \u043e\u0431\u043b\u0430\u043a\u0430", "local": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043b\u043e\u043a\u0430\u043b\u043d\u043e (\u0437\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u043d\u0435)" diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index 89e461c709d..940233bf405 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat", "already_configured_account": "El compte ja est\u00e0 configurat", "already_configured_device": "El dispositiu ja est\u00e0 configurat", "no_devices_found": "No s'han trobat dispositius a la xarxa", @@ -26,9 +25,6 @@ "description": "Vols configurar {model} ({device_id})?" }, "local": { - "data": { - "host": "Adre\u00e7a IP" - }, "description": "Segueix [aquestes instruccions]({url}) per com activar l'API local d'Awair.\n\nPrem 'envia' quan hagis acabat." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "Adre\u00e7a IP" } }, - "reauth": { - "data": { - "access_token": "Token d'acc\u00e9s", - "email": "Correu electr\u00f2nic" - }, - "description": "Torna a introduir el token d'acc\u00e9s de desenvolupador d'Awair." - }, "reauth_confirm": { "data": { "access_token": "Token d'acc\u00e9s", @@ -52,10 +41,6 @@ "description": "Torna a introduir el 'token' d'acc\u00e9s de desenvolupador d'Awair." }, "user": { - "data": { - "access_token": "Token d'acc\u00e9s", - "email": "Correu electr\u00f2nic" - }, "description": "Tria local per a la millor experi\u00e8ncia. Utilitza 'al n\u00favol' si el teu dispositiu no est\u00e0 connectat a la mateixa xarxa que Home Assistant, o si tens un dispositiu antic.", "menu_options": { "cloud": "Connecta't a trav\u00e9s del n\u00favol", diff --git a/homeassistant/components/awair/translations/cs.json b/homeassistant/components/awair/translations/cs.json index 6821388f2d4..7e2b5318288 100644 --- a/homeassistant/components/awair/translations/cs.json +++ b/homeassistant/components/awair/translations/cs.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_configured_account": "\u00da\u010det je ji\u017e nastaven", "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", @@ -24,27 +23,12 @@ "discovery_confirm": { "description": "Chcete nastavit {model} ({device_id})?" }, - "local": { - "data": { - "host": "IP adresa" - } - }, "local_pick": { "data": { "host": "IP adresa" } }, - "reauth": { - "data": { - "access_token": "P\u0159\u00edstupov\u00fd token", - "email": "E-mail" - } - }, "user": { - "data": { - "access_token": "P\u0159\u00edstupov\u00fd token", - "email": "E-mail" - }, "description": "Pro p\u0159\u00edstupov\u00fd token v\u00fdvoj\u00e1\u0159e Awair se mus\u00edte zaregistrovat na: https://developer.getawair.com/onboard/login" } } diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index af7aaaafff7..fc1baa30a46 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert", "already_configured_account": "Konto wurde bereits konfiguriert", "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", @@ -26,10 +25,7 @@ "description": "M\u00f6chtest du {model} ({device_id}) einrichten?" }, "local": { - "data": { - "host": "IP-Adresse" - }, - "description": "Befolge [diese Anweisungen]({url}), um die Awair Local API zu aktivieren. \n\nDr\u00fccke abschlie\u00dfend auf Senden." + "description": "Befolge [diese Anweisungen]({url}), um die Awair Lokal API zu aktivieren. \n\nDr\u00fccke abschlie\u00dfend auf Senden." }, "local_pick": { "data": { @@ -37,13 +33,6 @@ "host": "IP-Adresse" } }, - "reauth": { - "data": { - "access_token": "Zugangstoken", - "email": "E-Mail" - }, - "description": "Bitte gib deinen Awair-Entwicklerzugriffstoken erneut ein." - }, "reauth_confirm": { "data": { "access_token": "Zugangstoken", @@ -52,10 +41,6 @@ "description": "Bitte gib deinen Awair-Entwicklerzugriffstoken erneut ein." }, "user": { - "data": { - "access_token": "Zugangstoken", - "email": "E-Mail" - }, "description": "W\u00e4hle lokal f\u00fcr die beste Erfahrung. Verwende die Cloud nur, wenn das Ger\u00e4t nicht mit demselben Netzwerk wie Home Assistant verbunden ist oder wenn du ein \u00e4lteres Ger\u00e4t hast.", "menu_options": { "cloud": "Verbindung \u00fcber die Cloud", diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 187551f40a3..849dc24270f 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_configured_account": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_configured_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", @@ -26,9 +25,6 @@ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} ({device_id});" }, "local": { - "data": { - "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" - }, "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" }, "local_pick": { @@ -37,13 +33,6 @@ "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" } }, - "reauth": { - "data": { - "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "email": "Email" - }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." - }, "reauth_confirm": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", @@ -52,10 +41,6 @@ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." }, "user": { - "data": { - "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "email": "Email" - }, "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://developer.getawair.com/onboard/login", "menu_options": { "cloud": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 cloud", diff --git a/homeassistant/components/awair/translations/en.json b/homeassistant/components/awair/translations/en.json index dfc06d2346a..9746eac4ca1 100644 --- a/homeassistant/components/awair/translations/en.json +++ b/homeassistant/components/awair/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Account is already configured", "already_configured_account": "Account is already configured", "already_configured_device": "Device is already configured", "no_devices_found": "No devices found on the network", @@ -26,9 +25,6 @@ "description": "Do you want to setup {model} ({device_id})?" }, "local": { - "data": { - "host": "IP Address" - }, "description": "Follow [these instructions]({url}) on how to enable the Awair Local API.\n\nClick submit when done." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP Address" } }, - "reauth": { - "data": { - "access_token": "Access Token", - "email": "Email" - }, - "description": "Please re-enter your Awair developer access token." - }, "reauth_confirm": { "data": { "access_token": "Access Token", @@ -52,10 +41,6 @@ "description": "Please re-enter your Awair developer access token." }, "user": { - "data": { - "access_token": "Access Token", - "email": "Email" - }, "description": "Pick local for the best experience. Only use cloud if the device is not connected to the same network as Home Assistant, or if you have a legacy device.", "menu_options": { "cloud": "Connect via the cloud", diff --git a/homeassistant/components/awair/translations/es-419.json b/homeassistant/components/awair/translations/es-419.json index f487cd397c4..a38eb327d52 100644 --- a/homeassistant/components/awair/translations/es-419.json +++ b/homeassistant/components/awair/translations/es-419.json @@ -1,9 +1,6 @@ { "config": { "step": { - "reauth": { - "description": "Vuelva a ingresar su token de acceso de desarrollador de Awair." - }, "user": { "description": "Debe registrarse para obtener un token de acceso de desarrollador de Awair en: https://developer.getawair.com/onboard/login" } diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 1f2508ec6e3..0e1efdc6f49 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada", "already_configured_account": "La cuenta ya est\u00e1 configurada", "already_configured_device": "El dispositivo ya est\u00e1 configurado", "no_devices_found": "No se encontraron dispositivos en la red", @@ -26,9 +25,6 @@ "description": "\u00bfQuieres configurar {model} ({device_id})?" }, "local": { - "data": { - "host": "Direcci\u00f3n IP" - }, "description": "Sigue [estas instrucciones]({url}) sobre c\u00f3mo habilitar la API local de Awair. \n\nHaz clic en enviar cuando hayas terminado." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "Direcci\u00f3n IP" } }, - "reauth": { - "data": { - "access_token": "Token de acceso", - "email": "Correo electr\u00f3nico" - }, - "description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair." - }, "reauth_confirm": { "data": { "access_token": "Token de acceso", @@ -52,10 +41,6 @@ "description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair." }, "user": { - "data": { - "access_token": "Token de acceso", - "email": "Correo electr\u00f3nico" - }, "description": "Elige local para la mejor experiencia. Usa solo la nube si el dispositivo no est\u00e1 conectado a la misma red que Home Assistant, o si tienes un dispositivo heredado.", "menu_options": { "cloud": "Conectar a trav\u00e9s de la nube", diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index 327379ace6e..13f8b2115b6 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud", "already_configured_account": "Konto on juba seadistatud", "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", "no_devices_found": "V\u00f5rgust ei leitud Awair seadmeid", @@ -26,9 +25,6 @@ "description": "Kas seadistada {model} ({device_id})?" }, "local": { - "data": { - "host": "IP aadress" - }, "description": "J\u00e4rgi [neid juhiseid]({url}) Awair Local API lubamise kohta.\n\nKui oled l\u00f5petanud, kl\u00f5psa nuppu Esita." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP aadress" } }, - "reauth": { - "data": { - "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", - "email": "E-post" - }, - "description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba." - }, "reauth_confirm": { "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", @@ -52,10 +41,6 @@ "description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba." }, "user": { - "data": { - "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", - "email": "E-post" - }, "description": "Parima kogemuse saamiseks vali kohalik \u00fchendus. Kasuta pilve ainult siis, kui seade ei ole \u00fchendatud samasse v\u00f5rku kui Home Assistant v\u00f5i kui on vanem seade.", "menu_options": { "cloud": "Pilve\u00fchendus", diff --git a/homeassistant/components/awair/translations/fi.json b/homeassistant/components/awair/translations/fi.json index edbdcb1b086..f17986ff80a 100644 --- a/homeassistant/components/awair/translations/fi.json +++ b/homeassistant/components/awair/translations/fi.json @@ -5,11 +5,6 @@ "data": { "email": "S\u00e4hk\u00f6posti" } - }, - "local": { - "data": { - "host": "IP Osoite" - } } } } diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 5d9636eccee..d4b420ee7ae 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "already_configured_account": "Le compte est d\u00e9j\u00e0 configur\u00e9", "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", @@ -26,9 +25,6 @@ "description": "Voulez-vous configurer {model} ({device_id})\u00a0?" }, "local": { - "data": { - "host": "Adresse IP" - }, "description": "Suivez [ces instructions]({url}) pour activer l\u2019API locale Awair.\n\nCliquez sur \u00ab\u00a0Valider\u00a0\u00bb apr\u00e8s avoir termin\u00e9." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "Adresse IP" } }, - "reauth": { - "data": { - "access_token": "Jeton d'acc\u00e8s", - "email": "Courriel" - }, - "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." - }, "reauth_confirm": { "data": { "access_token": "Jeton d'acc\u00e8s", @@ -52,10 +41,6 @@ "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." }, "user": { - "data": { - "access_token": "Jeton d'acc\u00e8s", - "email": "Courriel" - }, "description": "S\u00e9lectionnez le mode local pour une exp\u00e9rience optimale. S\u00e9lectionnez le mode cloud uniquement si l'appareil n'est pas connect\u00e9 au m\u00eame r\u00e9seau que Home Assistant ou si vous disposez d'un appareil ancien.", "menu_options": { "cloud": "Connexion cloud", diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json index 56e562de0c5..f5a985463d1 100644 --- a/homeassistant/components/awair/translations/he.json +++ b/homeassistant/components/awair/translations/he.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { @@ -16,23 +15,11 @@ "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" } }, - "reauth": { - "data": { - "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", - "email": "\u05d3\u05d5\u05d0\"\u05dc" - } - }, "reauth_confirm": { "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", "email": "\u05d3\u05d5\u05d0\"\u05dc" } - }, - "user": { - "data": { - "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", - "email": "\u05d3\u05d5\u05d0\"\u05dc" - } } } } diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 2e7be72e57f..fd720372ac3 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", @@ -26,9 +25,6 @@ "description": "Be\u00e1ll\u00edtja a k\u00f6vetkez\u0151t: {model} ({device_id})?" }, "local": { - "data": { - "host": "IP c\u00edm" - }, "description": "K\u00f6vesse [az utas\u00edt\u00e1sokat]({url}) az Awair lok\u00e1lis API enged\u00e9lyez\u00e9s\u00e9hez. \n\n Ha k\u00e9sz, kattintson a MEHET gombra." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP c\u00edm" } }, - "reauth": { - "data": { - "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", - "email": "E-mail" - }, - "description": "Adja meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." - }, "reauth_confirm": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", @@ -52,10 +41,6 @@ "description": "Adja meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." }, "user": { - "data": { - "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", - "email": "E-mail" - }, "description": "V\u00e1lassza a helyi lehet\u0151s\u00e9get a legjobb \u00e9lm\u00e9ny \u00e9rdek\u00e9ben. Csak akkor haszn\u00e1lja a felh\u0151t, ha az eszk\u00f6z nem ugyanahhoz a h\u00e1l\u00f3zathoz csatlakozik, mint a Home Assistant, vagy ha r\u00e9gebbi eszk\u00f6zzel rendelkezik.", "menu_options": { "cloud": "Felh\u0151n kereszt\u00fcli csatlakoz\u00e1s", diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 3d633070f20..b613c3fb833 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi", "already_configured_account": "Akun sudah dikonfigurasi", "already_configured_device": "Perangkat sudah dikonfigurasi", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", @@ -26,9 +25,6 @@ "description": "Ingin menyiapkan {model} ({device_id})?" }, "local": { - "data": { - "host": "Alamat IP" - }, "description": "Ikuti [petunjuk ini]( {url} ) untuk mengaktifkan API Lokal Awair. \n\n Klik kirim setelah selesai." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "Alamat IP" } }, - "reauth": { - "data": { - "access_token": "Token Akses", - "email": "Email" - }, - "description": "Masukkan kembali token akses pengembang Awair Anda." - }, "reauth_confirm": { "data": { "access_token": "Token Akses", @@ -52,10 +41,6 @@ "description": "Masukkan kembali token akses pengembang Awair Anda." }, "user": { - "data": { - "access_token": "Token Akses", - "email": "Email" - }, "description": "Pilih lokal untuk pengalaman terbaik. Hanya gunakan opsi cloud jika perangkat tidak terhubung ke jaringan yang sama dengan Home Assistant, atau jika Anda memiliki perangkat versi lawas.", "menu_options": { "cloud": "Terhubung melalui cloud", diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index c8c1f1f6ec6..fb090e6d222 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato", "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", "no_devices_found": "Nessun dispositivo trovato sulla rete", @@ -26,9 +25,6 @@ "description": "Vuoi configurare {model} ({device_id})?" }, "local": { - "data": { - "host": "Indirizzo IP" - }, "description": "Segui [queste istruzioni]({url}) su come abilitare l'API Awair Local. \n\n Fai clic su Invia quando hai finito." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "Indirizzo IP" } }, - "reauth": { - "data": { - "access_token": "Token di accesso", - "email": "Email" - }, - "description": "Inserisci nuovamente il tuo token di accesso per sviluppatori Awair." - }, "reauth_confirm": { "data": { "access_token": "Token di accesso", @@ -52,10 +41,6 @@ "description": "Inserisci nuovamente il tuo token di accesso sviluppatore Awair." }, "user": { - "data": { - "access_token": "Token di accesso", - "email": "Email" - }, "description": "Scegli locale per la migliore esperienza. Utilizza il cloud solo se il dispositivo non \u00e8 connesso alla stessa rete di Home Assistant o se disponi di un dispositivo legacy.", "menu_options": { "cloud": "Connettiti tramite il cloud", diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 11c151082b9..a222204084a 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_configured_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", @@ -26,9 +25,6 @@ "description": "{model} ({device_id}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "local": { - "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9" - }, "description": "[\u3053\u308c\u3089\u306e\u624b\u9806]( {url} ) \u306b\u5f93\u3063\u3066\u3001Awair Local API \u3092\u6709\u52b9\u306b\u3059\u308b\u65b9\u6cd5\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n\u5b8c\u4e86\u3057\u305f\u3089\u9001\u4fe1\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP\u30a2\u30c9\u30ec\u30b9" } }, - "reauth": { - "data": { - "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", - "email": "E\u30e1\u30fc\u30eb" - }, - "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - }, "reauth_confirm": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", @@ -52,10 +41,6 @@ "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { - "data": { - "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", - "email": "E\u30e1\u30fc\u30eb" - }, "description": "Awair developer access token\u306e\u767b\u9332\u306f\u4ee5\u4e0b\u306e\u30b5\u30a4\u30c8\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://developer.getawair.com/onboard/login", "menu_options": { "cloud": "\u30af\u30e9\u30a6\u30c9\u7d4c\u7531\u3067\u63a5\u7d9a", diff --git a/homeassistant/components/awair/translations/ko.json b/homeassistant/components/awair/translations/ko.json index 22677f8ab45..0f9f06d9e46 100644 --- a/homeassistant/components/awair/translations/ko.json +++ b/homeassistant/components/awair/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, @@ -10,18 +9,7 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "reauth": { - "data": { - "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", - "email": "\uc774\uba54\uc77c" - }, - "description": "Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub2e4\uc2dc \uc785\ub825\ud574\uc8fc\uc138\uc694." - }, "user": { - "data": { - "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", - "email": "\uc774\uba54\uc77c" - }, "description": "https://developer.getawair.com/onboard/login \uc5d0 Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub4f1\ub85d\ud574\uc57c\ud569\ub2c8\ub2e4" } } diff --git a/homeassistant/components/awair/translations/lb.json b/homeassistant/components/awair/translations/lb.json index cb2f758113a..3807a8b35a8 100644 --- a/homeassistant/components/awair/translations/lb.json +++ b/homeassistant/components/awair/translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Kont ass", "no_devices_found": "Keng Apparater am Netzwierk fonnt", "reauth_successful": "Erfollegr\u00e4ich aktualis\u00e9iert" }, @@ -10,18 +9,7 @@ "unknown": "Onerwaarte Feeler" }, "step": { - "reauth": { - "data": { - "access_token": "Acc\u00e8s Jeton", - "email": "E-Mail" - }, - "description": "G\u00ebff d\u00e4in Awair Developpeur Acc\u00e8s jeton nach emol un." - }, "user": { - "data": { - "access_token": "Acc\u00e8s Jeton", - "email": "E-Mail" - }, "description": "Du muss dech fir een Awair Developpeur Acc\u00e8s Jeton registr\u00e9ien op:\nhttps://developer.getawair.com/onboard/login" } } diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 7c70960c1b1..c3d1e1d9583 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd", "already_configured_account": "Account is al geconfigureerd", "already_configured_device": "Apparaat is al geconfigureerd", "no_devices_found": "Geen apparaten gevonden op het netwerk", @@ -21,24 +20,12 @@ "email": "E-mail" } }, - "local": { - "data": { - "host": "IP-adres" - } - }, "local_pick": { "data": { "device": "Apparaat", "host": "IP-adres" } }, - "reauth": { - "data": { - "access_token": "Toegangstoken", - "email": "E-mail" - }, - "description": "Voer uw Awair-ontwikkelaarstoegangstoken opnieuw in." - }, "reauth_confirm": { "data": { "access_token": "Toegangstoken", @@ -46,10 +33,6 @@ } }, "user": { - "data": { - "access_token": "Toegangstoken", - "email": "E-mail" - }, "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login", "menu_options": { "cloud": "Verbinden via de cloud" diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 983a47ecfed..636060df9da 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert", "already_configured_account": "Kontoen er allerede konfigurert", "already_configured_device": "Enheten er allerede konfigurert", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", @@ -26,9 +25,6 @@ "description": "Vil du konfigurere {model} ( {device_id} )?" }, "local": { - "data": { - "host": "IP adresse" - }, "description": "F\u00f8lg [disse instruksjonene]( {url} ) om hvordan du aktiverer Awair Local API. \n\n Klikk p\u00e5 send n\u00e5r du er ferdig." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP adresse" } }, - "reauth": { - "data": { - "access_token": "Tilgangstoken", - "email": "E-post" - }, - "description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt." - }, "reauth_confirm": { "data": { "access_token": "Tilgangstoken", @@ -52,10 +41,6 @@ "description": "Skriv inn Awair-utviklertilgangstokenet ditt p\u00e5 nytt." }, "user": { - "data": { - "access_token": "Tilgangstoken", - "email": "E-post" - }, "description": "Velg lokal for den beste opplevelsen. Bruk bare sky hvis enheten ikke er koblet til samme nettverk som Home Assistant, eller hvis du har en eldre enhet.", "menu_options": { "cloud": "Koble til via skyen", diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index c42f9863b1c..072d8871daf 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane", "already_configured_account": "Konto jest ju\u017c skonfigurowane", "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", @@ -26,9 +25,6 @@ "description": "Czy chcesz skonfigurowa\u0107 {model} ({device_id})?" }, "local": { - "data": { - "host": "Adres IP" - }, "description": "Post\u0119puj zgodnie z [tymi instrukcjami]({url}), aby dowiedzie\u0107 si\u0119, jak w\u0142\u0105czy\u0107 lokalny interfejs API Awair. \n\n Po zako\u0144czeniu kliknij Zatwierd\u017a." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "Adres IP" } }, - "reauth": { - "data": { - "access_token": "Token dost\u0119pu", - "email": "Adres e-mail" - }, - "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." - }, "reauth_confirm": { "data": { "access_token": "Token dost\u0119pu", @@ -52,10 +41,6 @@ "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." }, "user": { - "data": { - "access_token": "Token dost\u0119pu", - "email": "Adres e-mail" - }, "description": "Wybierz lokalny, aby uzyska\u0107 najlepsze efekty. Korzystaj z chmury tylko wtedy, gdy urz\u0105dzenie nie jest pod\u0142\u0105czone do tej samej sieci co Home Assistant lub je\u015bli masz starsze urz\u0105dzenie.", "menu_options": { "cloud": "Po\u0142\u0105cz si\u0119 przez chmur\u0119", diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index 109f123f9e2..36b2fa61ebf 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 foi configurada", "already_configured_account": "A conta j\u00e1 foi configurada", "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", @@ -26,9 +25,6 @@ "description": "Deseja configurar {model} ({device_id})?" }, "local": { - "data": { - "host": "Endere\u00e7o IP" - }, "description": "Siga [estas instru\u00e7\u00f5es]({url}) sobre como ativar a API local Awair. \n\n Clique em enviar quando terminar." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "Endere\u00e7o IP" } }, - "reauth": { - "data": { - "access_token": "Token de acesso", - "email": "Email" - }, - "description": "Insira novamente seu token de acesso de desenvolvedor Awair." - }, "reauth_confirm": { "data": { "access_token": "Token de acesso", @@ -52,10 +41,6 @@ "description": "Insira novamente seu token de acesso de desenvolvedor Awair." }, "user": { - "data": { - "access_token": "Token de acesso", - "email": "Email" - }, "description": "Escolha local para a melhor experi\u00eancia. Use a nuvem apenas se o dispositivo n\u00e3o estiver conectado \u00e0 mesma rede que o Home Assistant ou se voc\u00ea tiver um dispositivo legado.", "menu_options": { "cloud": "Conecte-se pela nuvem", diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index 1f922776179..085b6dd056d 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada", "already_configured_account": "Conta j\u00e1 configurada", "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", @@ -26,9 +25,6 @@ "description": "Deseja configurar {model} ( {device_id} )?" }, "local": { - "data": { - "host": "Endere\u00e7o IP" - }, "description": "Siga [estas instru\u00e7\u00f5es]( {url} ) sobre como ativar a API local Awair. \n\n Clique em enviar quando terminar." }, "local_pick": { @@ -37,12 +33,6 @@ "host": "Endere\u00e7o IP" } }, - "reauth": { - "data": { - "access_token": "Token de Acesso", - "email": "Email" - } - }, "reauth_confirm": { "data": { "access_token": "Token de Acesso", @@ -50,10 +40,6 @@ } }, "user": { - "data": { - "access_token": "Token de Acesso", - "email": "Email" - }, "menu_options": { "cloud": "Conecte-se pela nuvem", "local": "Conecte-se localmente (preferencial)" diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index d59c4ed50f1..1725669af20 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -1,7 +1,6 @@ { "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.", "already_configured_account": "\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.", "already_configured_device": "\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.", "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.", @@ -26,9 +25,6 @@ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 {model} ({device_id})?" }, "local": { - "data": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441" - }, "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u044d\u0442\u0438\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({url}), \u0447\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP-\u0430\u0434\u0440\u0435\u0441" } }, - "reauth": { - "data": { - "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", - "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" - }, - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." - }, "reauth_confirm": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", @@ -52,10 +41,6 @@ "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." }, "user": { - "data": { - "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", - "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" - }, "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u043a\u043e, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0438\u043b\u0438 \u0435\u0441\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0442\u043e\u0439 \u0436\u0435 \u0441\u0435\u0442\u0438, \u0447\u0442\u043e \u0438 Home Assistant. \u0412 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 \u043e\u0442\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044e.", "menu_options": { "cloud": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043e\u0431\u043b\u0430\u043a\u043e", diff --git a/homeassistant/components/awair/translations/sk.json b/homeassistant/components/awair/translations/sk.json index dabf3e7e933..b8b771b2463 100644 --- a/homeassistant/components/awair/translations/sk.json +++ b/homeassistant/components/awair/translations/sk.json @@ -1,19 +1,44 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured_account": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_configured_device": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unreachable": "Nepodarilo sa pripoji\u0165" }, + "error": { + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", + "unreachable": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{model} ({device_id})", "step": { - "reauth": { + "cloud": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token", + "email": "Email" + } + }, + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {model} ({device_id})?" + }, + "local_pick": { + "data": { + "device": "Zaradenie", + "host": "IP adresa" + } + }, + "reauth_confirm": { "data": { "access_token": "Pr\u00edstupov\u00fd token", "email": "Email" } }, "user": { - "data": { - "access_token": "Pr\u00edstupov\u00fd token", - "email": "Email" + "menu_options": { + "cloud": "Pripojenie cez cloud", + "local": "Lok\u00e1lne pripojenie (preferovan\u00e9)" } } } diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index 3fbdec53f83..a0fc18ac01b 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats", "already_configured_account": "Konto har redan konfigurerats", "already_configured_device": "Enheten \u00e4r redan konfigurerad", "no_devices_found": "Inga enheter hittades i n\u00e4tverket", @@ -26,9 +25,6 @@ "description": "Vill du konfigurera {model} ( {device_id} )?" }, "local": { - "data": { - "host": "IP-adress" - }, "description": "F\u00f6lj [dessa instruktioner]({url}) om hur du aktiverar Awair Local API.\n\nKlicka p\u00e5 skicka n\u00e4r du \u00e4r klar." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP-adress" } }, - "reauth": { - "data": { - "access_token": "\u00c5tkomstnyckel", - "email": "E-post" - }, - "description": "Ange din Awair-utvecklar\u00e5tkomsttoken igen." - }, "reauth_confirm": { "data": { "access_token": "\u00c5tkomsttoken", @@ -52,10 +41,6 @@ "description": "Ange din Awair-utvecklar\u00e5tkomsttoken igen." }, "user": { - "data": { - "access_token": "\u00c5tkomstnyckel", - "email": "E-post" - }, "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: https://developer.getawair.com/onboard/login", "menu_options": { "cloud": "Anslut via molnet", diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index 87f90a564bc..7f9f025a566 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_configured_account": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", @@ -26,9 +25,6 @@ "description": "{model} ( {device_id} ) kurulumunu yapmak istiyor musunuz?" }, "local": { - "data": { - "host": "IP Adresi" - }, "description": "Awair Yerel API'sinin nas\u0131l etkinle\u015ftirilece\u011fiyle ilgili [bu talimatlar\u0131]( {url} ) uygulay\u0131n. \n\n \u0130\u015finiz bitti\u011finde g\u00f6nder'i t\u0131klay\u0131n." }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP Adresi" } }, - "reauth": { - "data": { - "access_token": "Eri\u015fim Anahtar\u0131", - "email": "E-posta" - }, - "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." - }, "reauth_confirm": { "data": { "access_token": "Eri\u015fim Anahtar\u0131", @@ -52,10 +41,6 @@ "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." }, "user": { - "data": { - "access_token": "Eri\u015fim Anahtar\u0131", - "email": "E-posta" - }, "description": "En iyi deneyim i\u00e7in yerel se\u00e7in. Bulutu yaln\u0131zca cihaz Home Assistant ile ayn\u0131 a\u011fa ba\u011fl\u0131 de\u011filse veya eski bir cihaz\u0131n\u0131z varsa kullan\u0131n.", "menu_options": { "cloud": "Bulut \u00fczerinden ba\u011flan\u0131n", diff --git a/homeassistant/components/awair/translations/uk.json b/homeassistant/components/awair/translations/uk.json index f8150ad7faf..d14e748df6f 100644 --- a/homeassistant/components/awair/translations/uk.json +++ b/homeassistant/components/awair/translations/uk.json @@ -1,7 +1,6 @@ { "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" }, @@ -10,18 +9,7 @@ "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" } } diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index fb6e6acf9cb..d97564cf778 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", @@ -26,9 +25,6 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {model} ({device_id})\uff1f" }, "local": { - "data": { - "host": "IP \u4f4d\u5740" - }, "description": "\u8ddf\u96a8 [\u4ee5\u4e0b\u6b65\u9a5f]({url}) \u4ee5\u555f\u7528 Awair \u672c\u5730\u7aef API\u3002\n\n\u5b8c\u6210\u5f8c\u9ede\u9078\u50b3\u9001\u3002" }, "local_pick": { @@ -37,13 +33,6 @@ "host": "IP \u4f4d\u5740" } }, - "reauth": { - "data": { - "access_token": "\u5b58\u53d6\u6b0a\u6756", - "email": "\u96fb\u5b50\u90f5\u4ef6" - }, - "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" - }, "reauth_confirm": { "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756", @@ -52,10 +41,6 @@ "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" }, "user": { - "data": { - "access_token": "\u5b58\u53d6\u6b0a\u6756", - "email": "\u96fb\u5b50\u90f5\u4ef6" - }, "description": "\u9078\u64c7\u672c\u5730\u7aef\u4ee5\u7372\u5f97\u6700\u4f73\u4f7f\u7528\u9ad4\u9a57\u3002\u50c5\u65bc\u88dd\u7f6e\u8207 Home Assistant \u4e26\u975e\u8655\u65bc\u76f8\u540c\u7db2\u8def\u4e0b\u3001\u6216\u8005\u4f7f\u7528\u820a\u8a2d\u5099\u7684\u60c5\u6cc1\u4e0b\u4f7f\u7528\u96f2\u7aef\u3002", "menu_options": { "cloud": "\u900f\u904e\u96f2\u7aef\u9023\u7dda", diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 1ce2f08c045..1fb9b9488fa 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -223,14 +223,10 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): return await self.async_step_user() -class AxisOptionsFlowHandler(config_entries.OptionsFlow): +class AxisOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): """Handle Axis device options.""" - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize Axis device options flow.""" - self.config_entry = config_entry - self.options = dict(config_entry.options) - self.device: AxisNetworkDevice | None = None + device: AxisNetworkDevice async def async_step_init( self, user_input: dict[str, Any] | None = None @@ -249,7 +245,6 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlow): schema = {} - assert self.device vapix = self.device.api.vapix # Stream profiles diff --git a/homeassistant/components/axis/translations/bg.json b/homeassistant/components/axis/translations/bg.json index 2cbf383cea8..e3ace757b0e 100644 --- a/homeassistant/components/axis/translations/bg.json +++ b/homeassistant/components/axis/translations/bg.json @@ -7,7 +7,8 @@ }, "error": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "Axis \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: {name} ({host})", "step": { diff --git a/homeassistant/components/axis/translations/sk.json b/homeassistant/components/axis/translations/sk.json index 53eb88bf838..b4415304f35 100644 --- a/homeassistant/components/axis/translations/sk.json +++ b/homeassistant/components/axis/translations/sk.json @@ -1,13 +1,23 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "link_local_address": "Lok\u00e1lne adresy odkazov nie s\u00fa podporovan\u00e9" + }, "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/azure_devops/translations/bg.json b/homeassistant/components/azure_devops/translations/bg.json index 28af7ef6e00..1b4e526e1b3 100644 --- a/homeassistant/components/azure_devops/translations/bg.json +++ b/homeassistant/components/azure_devops/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/azure_devops/translations/sk.json b/homeassistant/components/azure_devops/translations/sk.json index 71a7aea5018..bbfab69b01c 100644 --- a/homeassistant/components/azure_devops/translations/sk.json +++ b/homeassistant/components/azure_devops/translations/sk.json @@ -1,10 +1,30 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "project_error": "Nepodarilo sa z\u00edska\u0165 inform\u00e1cie o projekte." + }, + "flow_title": "{project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Osobn\u00fd pr\u00edstupov\u00fd token (PAT)" + }, + "description": "Overenie toto\u017enosti pre {project_url} zlyhalo. Zadajte svoje aktu\u00e1lne poverenia.", + "title": "Op\u00e4tovn\u00e9 overenie" + }, + "user": { + "data": { + "organization": "Organiz\u00e1cia", + "personal_access_token": "Osobn\u00fd pr\u00edstupov\u00fd token (PAT)", + "project": "Projekt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/config_flow.py b/homeassistant/components/azure_event_hub/config_flow.py index 26980231dc1..c789b85aebb 100644 --- a/homeassistant/components/azure_event_hub/config_flow.py +++ b/homeassistant/components/azure_event_hub/config_flow.py @@ -11,6 +11,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .client import AzureEventHubClient from .const import ( @@ -52,6 +56,15 @@ SAS_SCHEMA = vol.Schema( } ) +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_SEND_INTERVAL): int, + } +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + async def validate_data(data: dict[str, Any]) -> dict[str, str] | None: """Validate the input.""" @@ -81,9 +94,9 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> AEHOptionsFlowHandler: + ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" - return AEHOptionsFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -167,32 +180,3 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return None self._data.update(user_input) return await validate_data(self._data) - - -class AEHOptionsFlowHandler(config_entries.OptionsFlow): - """Handle azure event hub options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize AEH options flow.""" - self.config_entry = config_entry - self.options = deepcopy(dict(config_entry.options)) - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the AEH options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Required( - CONF_SEND_INTERVAL, - default=self.options.get(CONF_SEND_INTERVAL), - ): int - } - ), - last_step=True, - ) diff --git a/homeassistant/components/azure_event_hub/translations/sk.json b/homeassistant/components/azure_event_hub/translations/sk.json index 3f20d345b26..cf133735e3b 100644 --- a/homeassistant/components/azure_event_hub/translations/sk.json +++ b/homeassistant/components/azure_event_hub/translations/sk.json @@ -1,7 +1,23 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "conn_string": { + "description": "Zadajte re\u0165azec pripojenia pre: {event_hub_instance_name}", + "title": "Met\u00f3da re\u0165azca pripojenia" + }, + "user": { + "data": { + "use_connection_string": "Pou\u017eitie re\u0165azca pripojenia" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/baf/translations/sk.json b/homeassistant/components/baf/translations/sk.json index 5ca1b9820a9..95d21efd120 100644 --- a/homeassistant/components/baf/translations/sk.json +++ b/homeassistant/components/baf/translations/sk.json @@ -8,7 +8,11 @@ "cannot_connect": "Nepodarilo sa pripoji\u0165", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "flow_title": "{name} - {model} ({ip_address})", "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} - {model} ({ip_address})?" + }, "user": { "data": { "ip_address": "IP adresa" diff --git a/homeassistant/components/balboa/config_flow.py b/homeassistant/components/balboa/config_flow.py index c0301bc9892..e9f94795ea5 100644 --- a/homeassistant/components/balboa/config_flow.py +++ b/homeassistant/components/balboa/config_flow.py @@ -12,11 +12,24 @@ from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import _LOGGER, CONF_SYNC_TIME, DOMAIN DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_SYNC_TIME, default=False): bool, + } +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + async def validate_input(data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" @@ -47,9 +60,9 @@ class BalboaSpaClientFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" - return BalboaSpaClientOptionsFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -77,30 +90,3 @@ class BalboaSpaClientFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" - - -class BalboaSpaClientOptionsFlowHandler(config_entries.OptionsFlow): - """Handle Balboa Spa Client options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize Balboa Spa Client options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage Balboa Spa Client options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_SYNC_TIME, - default=self.config_entry.options.get(CONF_SYNC_TIME, False), - ): bool, - } - ), - ) diff --git a/homeassistant/components/balboa/translations/sk.json b/homeassistant/components/balboa/translations/sk.json new file mode 100644 index 00000000000..be996d9dacf --- /dev/null +++ b/homeassistant/components/balboa/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + }, + "title": "Pripojte sa k zariadeniu Balboa Wi-Fi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/is.json b/homeassistant/components/binary_sensor/translations/is.json index ac2c9901cad..72bc49a04c6 100644 --- a/homeassistant/components/binary_sensor/translations/is.json +++ b/homeassistant/components/binary_sensor/translations/is.json @@ -1,17 +1,52 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} rafhla\u00f0an er l\u00edtil", "is_co": "{entity_name} skynja\u00f0i kolm\u00f3noxi\u00f0", + "is_cold": "{entity_name} er kalt", + "is_connected": "{entity_name} er tengdur", "is_gas": "{entity_name} skynja\u00f0i gas", + "is_hot": "{entity_name} er heitt", "is_light": "{entity_name} skynja\u00f0i lj\u00f3s", + "is_locked": "{entity_name} er l\u00e6st", + "is_moist": "{entity_name} er rakt", "is_motion": "{entity_name} skynja\u00f0i hreyfingu", + "is_moving": "{entity_name} er \u00e1 hreyfingu", + "is_no_co": "{entity_name} er ekki a\u00f0 skynja kolm\u00f3nox\u00ed\u00f0", + "is_no_gas": "{entity_name} greinir ekki gas", + "is_no_light": "{entity_name} greinir ekki lj\u00f3s", "is_no_motion": "{entity_name} er ekki a\u00f0 skynja hreyfingu", + "is_no_problem": "{entity_name} greinir ekki vandam\u00e1l", "is_no_smoke": "{entity_name} er ekki a\u00f0 skynja reyk", + "is_no_sound": "{entity_name} greinir ekki hlj\u00f3\u00f0", + "is_no_update": "{entity_name} er uppf\u00e6rt", + "is_no_vibration": "{entity_name} greinir ekki titring", + "is_not_bat_low": "{entity_name} rafhla\u00f0a er e\u00f0lileg", + "is_not_cold": "{entity_name} er ekki kalt", + "is_not_connected": "{entity_name} er aftengt", + "is_not_locked": "{entity_name} er \u00f3l\u00e6st", + "is_not_moist": "{entity_name} er \u00feurr", + "is_not_moving": "{entity_name} hreyfist ekki", "is_not_open": "{entity_name} er loku\u00f0", + "is_not_plugged_in": "{entity_name} er aftengt", + "is_not_powered": "{entity_name} er ekki \u00ed gangi", + "is_not_present": "{entity_name} er ekki til sta\u00f0ar", + "is_not_running": "{entity_name} er ekki keyrandi", + "is_not_tampered": "{entity_name} skynja\u00f0i ekki fikt", + "is_not_unsafe": "{entity_name} er \u00f6ruggt", + "is_occupied": "{entity_name} er uppteki\u00f0", + "is_off": "{entity_name} er sl\u00f6kkt", + "is_on": "{entity_name} er \u00ed gangi", + "is_open": "{entity_name} er opin", + "is_plugged_in": "{entity_name} er tengdur", + "is_powered": "{entity_name} er \u00ed gangi", + "is_present": "{entity_name} er til sta\u00f0ar", "is_problem": "{entity_name} skynja\u00f0i vandam\u00e1l", + "is_running": "{entity_name} er keyrandi", "is_smoke": "{entity_name} skynja\u00f0i reyk", "is_sound": "{entity_name} skynja\u00f0i hlj\u00f3\u00f0", "is_tampered": "{entity_name} skynja\u00f0i fikt", + "is_unsafe": "{entity_name} er \u00f3\u00f6ruggt", "is_vibration": "{entity_name} skynja\u00f0i titring" }, "trigger_type": { diff --git a/homeassistant/components/binary_sensor/translations/sk.json b/homeassistant/components/binary_sensor/translations/sk.json index 89e6288a979..c6d0666cffb 100644 --- a/homeassistant/components/binary_sensor/translations/sk.json +++ b/homeassistant/components/binary_sensor/translations/sk.json @@ -1,6 +1,95 @@ { + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} bat\u00e9ria je vybit\u00e1", + "is_co": "{entity_name} zis\u0165uje oxid uho\u013enat\u00fd", + "is_cold": "{entity_name} je studen\u00e9", + "is_connected": "{entity_name} je pripojen\u00fd", + "is_gas": "{entity_name} detekuje plyn", + "is_hot": "{entity_name} je hor\u00face", + "is_light": "{entity_name} zaznamen\u00e1va svetlo", + "is_locked": "{entity_name} je uzamknut\u00e9", + "is_motion": "{entity_name} zaznamen\u00e1va pohyb", + "is_moving": "{entity_name} sa h\u00fdbe", + "is_no_co": "{entity_name} nezaznamen\u00e1va oxid uho\u013enat\u00fd", + "is_no_gas": "{entity_name} nedetekuje plyn", + "is_no_light": "{entity_name} nezaznamen\u00e1va svetlo", + "is_no_motion": "{entity_name} nezaznamen\u00e1va pohyb", + "is_no_problem": "{entity_name} nedetekuje probl\u00e9m", + "is_no_smoke": "{entity_name} nezis\u0165uje dym", + "is_no_sound": "{entity_name} nerozpozn\u00e1va zvuk", + "is_no_update": "{entity_name} je aktu\u00e1lne", + "is_no_vibration": "{entity_name} nezaznamen\u00e1va vibr\u00e1cie", + "is_not_bat_low": "{entity_name} bat\u00e9ria je norm\u00e1lna", + "is_not_cold": "{entity_name} nie je studen\u00e9", + "is_not_connected": "{entity_name} je odpojen\u00fd", + "is_not_hot": "{entity_name} nie je hor\u00face", + "is_not_locked": "{entity_name} je odomknut\u00e9", + "is_not_moist": "{entity_name} je such\u00e9", + "is_not_moving": "{entity_name} sa neh\u00fdbe", + "is_not_occupied": "{entity_name} nie je obsaden\u00e1", + "is_not_open": "{entity_name} je zatvoren\u00e9", + "is_not_plugged_in": "{entity_name} je odpojen\u00e9", + "is_not_powered": "{entity_name} nie je nap\u00e1jan\u00e9", + "is_not_present": "{entity_name} nie je pr\u00edtomn\u00e9", + "is_not_running": "{entity_name} nie je spusten\u00e9", + "is_not_tampered": "{entity_name} nedetekuje manipul\u00e1ciu", + "is_occupied": "{entity_name} je obsaden\u00e9", + "is_off": "{entity_name} je vypnut\u00e9", + "is_on": "{entity_name} je zapnut\u00e9", + "is_open": "{entity_name} je otvoren\u00e1", + "is_plugged_in": "{entity_name} je zapojen\u00e9", + "is_powered": "{entity_name} je nap\u00e1jan\u00e9", + "is_present": "{entity_name} je pr\u00edtomn\u00e9", + "is_problem": "{entity_name} detekuje probl\u00e9m", + "is_running": "{entity_name} je spusten\u00e9", + "is_smoke": "{entity_name} zis\u0165uje dym", + "is_sound": "{entity_name} rozpozn\u00e1va zvuk", + "is_tampered": "{entity_name} detekuje manipul\u00e1ciu", + "is_update": "{entity_name} m\u00e1 k dispoz\u00edcii aktualiz\u00e1ciu", + "is_vibration": "{entity_name} zaznamen\u00e1va vibr\u00e1cie" + }, + "trigger_type": { + "bat_low": "{entity_name} bat\u00e9ria vybit\u00e1", + "co": "{entity_name} zis\u0165uje oxid uho\u013enat\u00fd", + "cold": "{entity_name} sa ochladilo", + "connected": "{entity_name} je pripojen\u00fd", + "gas": "{entity_name} detekuje plyn", + "hot": "{n\u00e1zov_objektu} sa stal hor\u00facim", + "light": "{entity_name} za\u010dal detekova\u0165 svetlo", + "moist": "{entity_name} sa stal vlhk\u00fdm", + "motion": "{entity_name} za\u010dal zis\u0165ova\u0165 pohyb", + "not_bat_low": "{entity_name} bat\u00e9ria norm\u00e1lna", + "not_connected": "{entity_name} odpojen\u00fd", + "not_hot": "{entity_name} prestal by\u0165 hor\u00facim", + "not_locked": "{entity_name} odomknut\u00e9", + "not_moist": "{n\u00e1zov_objektu} sa stal such\u00fdm", + "not_opened": "{entity_name} zatvoren\u00e9", + "not_plugged_in": "{entity_name} odpojen\u00fd", + "not_powered": "{entity_name} nie je nap\u00e1jan\u00e9", + "not_present": "{entity_name} nie je pr\u00edtomn\u00e9", + "not_running": "{entity_name} u\u017e nie je v prev\u00e1dzke", + "opened": "{entity_name} otvoren\u00e9", + "plugged_in": "{entity_name} zapojen\u00e9", + "powered": "{entity_name} nap\u00e1jan\u00e9", + "problem": "{entity_name} za\u010dal zis\u0165ova\u0165 probl\u00e9m", + "smoke": "{entity_name} za\u010dala zis\u0165ova\u0165 dym", + "sound": "{entity_name} za\u010dala rozpozn\u00e1va\u0165 zvuk", + "update": "{entity_name} m\u00e1 k dispoz\u00edcii aktualiz\u00e1ciu" + } + }, "device_class": { + "co": "oxid uho\u013enat\u00fd", + "cold": "chlad", + "gas": "plyn", + "heat": "teplo", + "moisture": "vlhkos\u0165", "motion": "pohyb", + "occupancy": "obsadenos\u0165", + "power": "nap\u00e1janie", + "problem": "probl\u00e9m", + "smoke": "dym", + "sound": "zvuk", "vibration": "vibr\u00e1cia" }, "state": { @@ -12,6 +101,14 @@ "off": "Norm\u00e1lna", "on": "Slab\u00e1" }, + "battery_charging": { + "off": "Nenab\u00edja sa", + "on": "Nab\u00edja sa" + }, + "carbon_monoxide": { + "off": "\u017diadny plyn", + "on": "Zachyten\u00fd plyn" + }, "cold": { "off": "Norm\u00e1lny", "on": "Studen\u00fd" @@ -36,6 +133,10 @@ "off": "Norm\u00e1lny", "on": "Hor\u00faci" }, + "light": { + "off": "Bez svetla", + "on": "Detekovan\u00e9 svetlo" + }, "lock": { "off": "Zamknut\u00fd", "on": "Odomknut\u00fd" @@ -56,6 +157,10 @@ "off": "Zatvoren\u00e9", "on": "Otvoren\u00e9" }, + "plug": { + "off": "Odpojen\u00fd", + "on": "Zapojen\u00fd" + }, "presence": { "off": "Pre\u010d", "on": "Doma" @@ -64,6 +169,10 @@ "off": "OK", "on": "Probl\u00e9m" }, + "running": { + "off": "Nie je spusten\u00e9", + "on": "Spusten\u00e9" + }, "safety": { "off": "Zabezpe\u010den\u00e9", "on": "Nezabezpe\u010den\u00e9" @@ -76,6 +185,10 @@ "off": "Ticho", "on": "Zachyten\u00fd zvuk" }, + "update": { + "off": "Aktu\u00e1lne", + "on": "Aktualiz\u00e1cia k dispoz\u00edcii" + }, "vibration": { "off": "K\u013eud", "on": "Zachyten\u00e9 vibr\u00e1cie" diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json index c104a96fe46..2b13adf2d69 100644 --- a/homeassistant/components/blebox/translations/de.json +++ b/homeassistant/components/blebox/translations/de.json @@ -9,7 +9,7 @@ "unknown": "Unerwarteter Fehler", "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisiere es zuerst." }, - "flow_title": "{name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blebox/translations/sk.json b/homeassistant/components/blebox/translations/sk.json index 892b8b2cd91..fa1e3a90bbd 100644 --- a/homeassistant/components/blebox/translations/sk.json +++ b/homeassistant/components/blebox/translations/sk.json @@ -1,8 +1,19 @@ { "config": { + "abort": { + "address_already_configured": "Zariadenie BleBox je u\u017e nakonfigurovan\u00e9 na {address}.", + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", + "unsupported_version": "Zariadenie BleBox m\u00e1 zastaran\u00fd firmv\u00e9r. Najprv ho aktualizujte." + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { + "host": "IP adresa", "port": "Port" } } diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 0c18950da66..ef5a99356bc 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -30,7 +30,7 @@ SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string}) -def _blink_startup_wrapper(hass, entry): +def _blink_startup_wrapper(hass: HomeAssistant, entry: ConfigEntry) -> Blink: """Startup wrapper for blink.""" blink = Blink() auth_data = deepcopy(dict(entry.data)) @@ -87,6 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) def blink_refresh(event_time=None): """Call blink to refresh info.""" @@ -116,7 +117,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback -def _async_import_options_from_data_if_missing(hass, entry): +def _async_import_options_from_data_if_missing( + hass: HomeAssistant, entry: ConfigEntry +) -> None: options = dict(entry.options) if CONF_SCAN_INTERVAL not in entry.options: options[CONF_SCAN_INTERVAL] = entry.data.get( @@ -144,6 +147,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + blink: Blink = hass.data[DOMAIN][entry.entry_id] + blink.refresh_rate = entry.options[CONF_SCAN_INTERVAL] + + async def async_handle_save_video_service(hass, entry, call): """Handle save video service calls.""" camera_name = call.data[CONF_NAME] diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index 4f1c1997cad..30ef294f515 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -18,11 +18,35 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN _LOGGER = logging.getLogger(__name__) +SIMPLE_OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + unit_of_measurement="seconds", + ), + ), + } +) + + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(next_step="simple_options"), + "simple_options": SchemaFlowFormStep(SIMPLE_OPTIONS_SCHEMA), +} + def validate_input(hass: core.HomeAssistant, auth): """Validate the user input allows us to connect.""" @@ -56,9 +80,9 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> BlinkOptionsFlowHandler: + ) -> SchemaOptionsFlowHandler: """Get options flow for this handler.""" - return BlinkOptionsFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" @@ -133,45 +157,6 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=DOMAIN, data=self.auth.login_attributes) -class BlinkOptionsFlowHandler(config_entries.OptionsFlow): - """Handle Blink options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize Blink options flow.""" - self.config_entry = config_entry - self.options = dict(config_entry.options) - self.blink = None - - async def async_step_init(self, user_input=None): - """Manage the Blink options.""" - self.blink = self.hass.data[DOMAIN][self.config_entry.entry_id] - self.options[CONF_SCAN_INTERVAL] = self.blink.refresh_rate - - return await self.async_step_simple_options() - - async def async_step_simple_options(self, user_input=None): - """For simple options.""" - if user_input is not None: - self.options.update(user_input) - self.blink.refresh_rate = user_input[CONF_SCAN_INTERVAL] - return self.async_create_entry(title="", data=self.options) - - options = self.config_entry.options - scan_interval = options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - - return self.async_show_form( - step_id="simple_options", - data_schema=vol.Schema( - { - vol.Optional( - CONF_SCAN_INTERVAL, - default=scan_interval, - ): int - } - ), - ) - - class Require2FA(exceptions.HomeAssistantError): """Error to indicate we require 2FA.""" diff --git a/homeassistant/components/blink/translations/bg.json b/homeassistant/components/blink/translations/bg.json index 60e7c86f621..c302ce972b9 100644 --- a/homeassistant/components/blink/translations/bg.json +++ b/homeassistant/components/blink/translations/bg.json @@ -14,7 +14,7 @@ "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u041f\u0418\u041d \u043a\u043e\u0434\u0430, \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0438\u043c\u0435\u0439\u043b", - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/blink/translations/sk.json b/homeassistant/components/blink/translations/sk.json index 5ada995aa6e..6057b8ea9d4 100644 --- a/homeassistant/components/blink/translations/sk.json +++ b/homeassistant/components/blink/translations/sk.json @@ -1,7 +1,38 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "2fa": { + "data": { + "2fa": "Dvojfaktorov\u00fd k\u00f3d" + }, + "description": "Zadajte PIN zaslan\u00fd na v\u00e1\u0161 e-mail", + "title": "Dvojfaktorov\u00e1 autentifik\u00e1cia" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Prihl\u00e1ste sa pomocou \u00fa\u010dtu Blink" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Interval skenovania (sekundy)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/device.py b/homeassistant/components/bluemaestro/device.py index 3d6e4546882..19d955dd945 100644 --- a/homeassistant/components/bluemaestro/device.py +++ b/homeassistant/components/bluemaestro/device.py @@ -1,13 +1,11 @@ """Support for BlueMaestro devices.""" from __future__ import annotations -from bluemaestro_ble import DeviceKey, SensorDeviceInfo +from bluemaestro_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo def device_key_to_bluetooth_entity_key( @@ -15,17 +13,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a bluemaestro device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py index 8afdef48d51..7fff348d587 100644 --- a/homeassistant/components/bluemaestro/sensor.py +++ b/homeassistant/components/bluemaestro/sensor.py @@ -31,9 +31,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { (BlueMaestroSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( @@ -96,7 +97,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/bluemaestro/translations/he.json b/homeassistant/components/bluemaestro/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/bluemaestro/translations/he.json +++ b/homeassistant/components/bluemaestro/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bluemaestro/translations/sk.json b/homeassistant/components/bluemaestro/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 833050ba089..7c23e76385a 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -775,10 +775,10 @@ class BluesoundPlayer(MediaPlayerEntity): return None @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag of media commands that are supported.""" if self._status is None: - return 0 + return MediaPlayerEntityFeature(0) if self.is_grouped and not self.is_master: return ( diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8590d1ad90a..8386178f459 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,15 +1,25 @@ """The bluetooth integration.""" from __future__ import annotations -from asyncio import Future -from collections.abc import Callable, Iterable import datetime import logging import platform -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING -import async_timeout from awesomeversion import AwesomeVersion +from bluetooth_adapters import ( + ADAPTER_ADDRESS, + ADAPTER_HW_VERSION, + ADAPTER_MANUFACTURER, + ADAPTER_SW_VERSION, + DEFAULT_ADDRESS, + AdapterDetails, + adapter_human_name, + adapter_model, + adapter_unique_name, + get_adapters, +) +from home_assistant_bluetooth import BluetoothServiceInfo, BluetoothServiceInfoBleak from homeassistant.components import usb from homeassistant.config_entries import ( @@ -18,7 +28,7 @@ from homeassistant.config_entries import ( ConfigEntry, ) from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback +from homeassistant.core import HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.debounce import Debouncer @@ -31,44 +41,45 @@ from homeassistant.helpers.issue_registry import ( from homeassistant.loader import async_get_bluetooth from . import models +from .api import ( + _get_manager, + async_address_present, + async_ble_device_from_address, + async_discovered_service_info, + async_get_advertisement_callback, + async_get_scanner, + async_last_service_info, + async_process_advertisements, + async_rediscover_address, + async_register_callback, + async_register_scanner, + async_scanner_by_source, + async_scanner_count, + async_track_unavailable, +) +from .base_scanner import BaseHaRemoteScanner, BaseHaScanner from .const import ( - ADAPTER_ADDRESS, - ADAPTER_HW_VERSION, - ADAPTER_SW_VERSION, BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DATA_MANAGER, - DEFAULT_ADDRESS, DOMAIN, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, - AdapterDetails, ) from .manager import BluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher -from .models import ( - BaseHaScanner, - BluetoothCallback, - BluetoothChange, - BluetoothScanningMode, - BluetoothServiceInfo, - BluetoothServiceInfoBleak, - HaBleakScannerWrapper, - HaBluetoothConnector, - ProcessAdvertisementCallback, -) +from .models import BluetoothCallback, BluetoothChange, BluetoothScanningMode from .scanner import HaScanner, ScannerStartError -from .util import adapter_human_name, adapter_unique_name, async_default_adapter +from .wrappers import HaBluetoothConnector if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from homeassistant.helpers.typing import ConfigType __all__ = [ + "async_address_present", "async_ble_device_from_address", "async_discovered_service_info", "async_get_scanner", @@ -78,8 +89,12 @@ __all__ = [ "async_register_callback", "async_register_scanner", "async_track_unavailable", + "async_scanner_by_source", "async_scanner_count", "BaseHaScanner", + "BaseHaRemoteScanner", + "BluetoothCallbackMatcher", + "BluetoothChange", "BluetoothServiceInfo", "BluetoothServiceInfoBleak", "BluetoothScanningMode", @@ -94,151 +109,7 @@ _LOGGER = logging.getLogger(__name__) RECOMMENDED_MIN_HAOS_VERSION = AwesomeVersion("9.0.dev0") -def _get_manager(hass: HomeAssistant) -> BluetoothManager: - """Get the bluetooth manager.""" - return cast(BluetoothManager, hass.data[DATA_MANAGER]) - - -@hass_callback -def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: - """Return a HaBleakScannerWrapper. - - This is a wrapper around our BleakScanner singleton that allows - multiple integrations to share the same BleakScanner. - """ - return HaBleakScannerWrapper() - - -@hass_callback -def async_scanner_count(hass: HomeAssistant, connectable: bool = True) -> int: - """Return the number of scanners currently in use.""" - return _get_manager(hass).async_scanner_count(connectable) - - -@hass_callback -def async_discovered_service_info( - hass: HomeAssistant, connectable: bool = True -) -> Iterable[BluetoothServiceInfoBleak]: - """Return the discovered devices list.""" - if DATA_MANAGER not in hass.data: - return [] - return _get_manager(hass).async_discovered_service_info(connectable) - - -@hass_callback -def async_last_service_info( - hass: HomeAssistant, address: str, connectable: bool = True -) -> BluetoothServiceInfoBleak | None: - """Return the last service info for an address.""" - if DATA_MANAGER not in hass.data: - return None - return _get_manager(hass).async_last_service_info(address, connectable) - - -@hass_callback -def async_ble_device_from_address( - hass: HomeAssistant, address: str, connectable: bool = True -) -> BLEDevice | None: - """Return BLEDevice for an address if its present.""" - if DATA_MANAGER not in hass.data: - return None - return _get_manager(hass).async_ble_device_from_address(address, connectable) - - -@hass_callback -def async_address_present( - hass: HomeAssistant, address: str, connectable: bool = True -) -> bool: - """Check if an address is present in the bluetooth device list.""" - if DATA_MANAGER not in hass.data: - return False - return _get_manager(hass).async_address_present(address, connectable) - - -@hass_callback -def async_register_callback( - hass: HomeAssistant, - callback: BluetoothCallback, - match_dict: BluetoothCallbackMatcher | None, - mode: BluetoothScanningMode, -) -> Callable[[], None]: - """Register to receive a callback on bluetooth change. - - mode is currently not used as we only support active scanning. - Passive scanning will be available in the future. The flag - is required to be present to avoid a future breaking change - when we support passive scanning. - - Returns a callback that can be used to cancel the registration. - """ - return _get_manager(hass).async_register_callback(callback, match_dict) - - -async def async_process_advertisements( - hass: HomeAssistant, - callback: ProcessAdvertisementCallback, - match_dict: BluetoothCallbackMatcher, - mode: BluetoothScanningMode, - timeout: int, -) -> BluetoothServiceInfoBleak: - """Process advertisements until callback returns true or timeout expires.""" - done: Future[BluetoothServiceInfoBleak] = Future() - - @hass_callback - def _async_discovered_device( - service_info: BluetoothServiceInfoBleak, change: BluetoothChange - ) -> None: - if not done.done() and callback(service_info): - done.set_result(service_info) - - unload = _get_manager(hass).async_register_callback( - _async_discovered_device, match_dict - ) - - try: - async with async_timeout.timeout(timeout): - return await done - finally: - unload() - - -@hass_callback -def async_track_unavailable( - hass: HomeAssistant, - callback: Callable[[BluetoothServiceInfoBleak], None], - address: str, - connectable: bool = True, -) -> Callable[[], None]: - """Register to receive a callback when an address is unavailable. - - Returns a callback that can be used to cancel the registration. - """ - return _get_manager(hass).async_track_unavailable(callback, address, connectable) - - -@hass_callback -def async_rediscover_address(hass: HomeAssistant, address: str) -> None: - """Trigger discovery of devices which have already been seen.""" - _get_manager(hass).async_rediscover_address(address) - - -@hass_callback -def async_register_scanner( - hass: HomeAssistant, scanner: BaseHaScanner, connectable: bool -) -> CALLBACK_TYPE: - """Register a BleakScanner.""" - return _get_manager(hass).async_register_scanner(scanner, connectable) - - -@hass_callback -def async_get_advertisement_callback( - hass: HomeAssistant, -) -> Callable[[BluetoothServiceInfoBleak], None]: - """Get the advertisement callback.""" - return _get_manager(hass).scanner_adv_received - - -async def async_get_adapter_from_address( +async def _async_get_adapter_from_address( hass: HomeAssistant, address: str ) -> str | None: """Get an adapter by the address.""" @@ -286,13 +157,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) integration_matcher.async_setup() - manager = BluetoothManager(hass, integration_matcher) + bluetooth_adapters = get_adapters() + manager = BluetoothManager(hass, integration_matcher, bluetooth_adapters) await manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager adapters = await manager.async_get_bluetooth_adapters() - async_migrate_entries(hass, adapters) + async_migrate_entries(hass, adapters, bluetooth_adapters.default_adapter) await async_discover_adapters(hass, adapters) async def _async_rediscover_adapters() -> None: @@ -345,17 +217,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: EVENT_HOMEASSISTANT_STARTED, hass_callback(lambda event: _async_check_haos(hass)), ) - return True @hass_callback def async_migrate_entries( - hass: HomeAssistant, adapters: dict[str, AdapterDetails] + hass: HomeAssistant, adapters: dict[str, AdapterDetails], default_adapter: str ) -> None: """Migrate config entries to support multiple.""" current_entries = hass.config_entries.async_entries(DOMAIN) - default_adapter = async_default_adapter() for entry in current_entries: if entry.unique_id: @@ -408,6 +278,8 @@ async def async_update_device( config_entry_id=entry.entry_id, name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]), connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])}, + manufacturer=details[ADAPTER_MANUFACTURER], + model=adapter_model(details), sw_version=details.get(ADAPTER_SW_VERSION), hw_version=details.get(ADAPTER_HW_VERSION), ) @@ -417,7 +289,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for a bluetooth scanner.""" address = entry.unique_id assert address is not None - adapter = await async_get_adapter_from_address(hass, address) + adapter = await _async_get_adapter_from_address(hass, address) if adapter is None: raise ConfigEntryNotReady( f"Bluetooth adapter {adapter} with address {address} not found" @@ -425,15 +297,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE - scanner = HaScanner(hass, mode, adapter, address) + new_info_callback = async_get_advertisement_callback(hass) + scanner = HaScanner(hass, mode, adapter, address, new_info_callback) try: scanner.async_setup() except RuntimeError as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" ) from err - info_callback = async_get_advertisement_callback(hass) - entry.async_on_unload(scanner.async_register_callback(info_callback)) try: await scanner.async_start() except ScannerStartError as err: diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py new file mode 100644 index 00000000000..582370ffbda --- /dev/null +++ b/homeassistant/components/bluetooth/api.py @@ -0,0 +1,186 @@ +"""The bluetooth integration apis. + +These APIs are the only documented way to interact with the bluetooth integration. +""" +from __future__ import annotations + +from asyncio import Future +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, cast + +import async_timeout +from home_assistant_bluetooth import BluetoothServiceInfoBleak + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback + +from .base_scanner import BaseHaScanner +from .const import DATA_MANAGER +from .manager import BluetoothManager +from .match import BluetoothCallbackMatcher +from .models import ( + BluetoothCallback, + BluetoothChange, + BluetoothScanningMode, + ProcessAdvertisementCallback, +) +from .wrappers import HaBleakScannerWrapper + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + +def _get_manager(hass: HomeAssistant) -> BluetoothManager: + """Get the bluetooth manager.""" + return cast(BluetoothManager, hass.data[DATA_MANAGER]) + + +@hass_callback +def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: + """Return a HaBleakScannerWrapper. + + This is a wrapper around our BleakScanner singleton that allows + multiple integrations to share the same BleakScanner. + """ + return HaBleakScannerWrapper() + + +@hass_callback +def async_scanner_by_source(hass: HomeAssistant, source: str) -> BaseHaScanner | None: + """Return a scanner for a given source. + + This method is only intended to be used by integrations that implement + a bluetooth client and need to interact with a scanner directly. + + It is not intended to be used by integrations that need to interact + with a device. + """ + return _get_manager(hass).async_scanner_by_source(source) + + +@hass_callback +def async_scanner_count(hass: HomeAssistant, connectable: bool = True) -> int: + """Return the number of scanners currently in use.""" + return _get_manager(hass).async_scanner_count(connectable) + + +@hass_callback +def async_discovered_service_info( + hass: HomeAssistant, connectable: bool = True +) -> Iterable[BluetoothServiceInfoBleak]: + """Return the discovered devices list.""" + if DATA_MANAGER not in hass.data: + return [] + return _get_manager(hass).async_discovered_service_info(connectable) + + +@hass_callback +def async_last_service_info( + hass: HomeAssistant, address: str, connectable: bool = True +) -> BluetoothServiceInfoBleak | None: + """Return the last service info for an address.""" + if DATA_MANAGER not in hass.data: + return None + return _get_manager(hass).async_last_service_info(address, connectable) + + +@hass_callback +def async_ble_device_from_address( + hass: HomeAssistant, address: str, connectable: bool = True +) -> BLEDevice | None: + """Return BLEDevice for an address if its present.""" + if DATA_MANAGER not in hass.data: + return None + return _get_manager(hass).async_ble_device_from_address(address, connectable) + + +@hass_callback +def async_address_present( + hass: HomeAssistant, address: str, connectable: bool = True +) -> bool: + """Check if an address is present in the bluetooth device list.""" + if DATA_MANAGER not in hass.data: + return False + return _get_manager(hass).async_address_present(address, connectable) + + +@hass_callback +def async_register_callback( + hass: HomeAssistant, + callback: BluetoothCallback, + match_dict: BluetoothCallbackMatcher | None, + mode: BluetoothScanningMode, +) -> Callable[[], None]: + """Register to receive a callback on bluetooth change. + + mode is currently not used as we only support active scanning. + Passive scanning will be available in the future. The flag + is required to be present to avoid a future breaking change + when we support passive scanning. + + Returns a callback that can be used to cancel the registration. + """ + return _get_manager(hass).async_register_callback(callback, match_dict) + + +async def async_process_advertisements( + hass: HomeAssistant, + callback: ProcessAdvertisementCallback, + match_dict: BluetoothCallbackMatcher, + mode: BluetoothScanningMode, + timeout: int, +) -> BluetoothServiceInfoBleak: + """Process advertisements until callback returns true or timeout expires.""" + done: Future[BluetoothServiceInfoBleak] = Future() + + @hass_callback + def _async_discovered_device( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange + ) -> None: + if not done.done() and callback(service_info): + done.set_result(service_info) + + unload = _get_manager(hass).async_register_callback( + _async_discovered_device, match_dict + ) + + try: + async with async_timeout.timeout(timeout): + return await done + finally: + unload() + + +@hass_callback +def async_track_unavailable( + hass: HomeAssistant, + callback: Callable[[BluetoothServiceInfoBleak], None], + address: str, + connectable: bool = True, +) -> Callable[[], None]: + """Register to receive a callback when an address is unavailable. + + Returns a callback that can be used to cancel the registration. + """ + return _get_manager(hass).async_track_unavailable(callback, address, connectable) + + +@hass_callback +def async_rediscover_address(hass: HomeAssistant, address: str) -> None: + """Trigger discovery of devices which have already been seen.""" + _get_manager(hass).async_rediscover_address(address) + + +@hass_callback +def async_register_scanner( + hass: HomeAssistant, scanner: BaseHaScanner, connectable: bool +) -> CALLBACK_TYPE: + """Register a BleakScanner.""" + return _get_manager(hass).async_register_scanner(scanner, connectable) + + +@hass_callback +def async_get_advertisement_callback( + hass: HomeAssistant, +) -> Callable[[BluetoothServiceInfoBleak], None]: + """Get the advertisement callback.""" + return _get_manager(hass).scanner_adv_received diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py new file mode 100644 index 00000000000..6204897c35c --- /dev/null +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -0,0 +1,228 @@ +"""Base classes for HA Bluetooth scanners for bluetooth.""" +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable, Generator +from contextlib import contextmanager +import datetime +from datetime import timedelta +from typing import Any, Final + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData +from bleak_retry_connector import NO_RSSI_VALUE +from bluetooth_adapters import adapter_human_name +from home_assistant_bluetooth import BluetoothServiceInfoBleak + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse + +from .const import ( + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) +from .models import HaBluetoothConnector + +MONOTONIC_TIME: Final = monotonic_time_coarse + + +class BaseHaScanner: + """Base class for Ha Scanners.""" + + __slots__ = ("hass", "source", "_connecting", "name", "scanning") + + def __init__(self, hass: HomeAssistant, source: str, adapter: str) -> None: + """Initialize the scanner.""" + self.hass = hass + self.source = source + self._connecting = 0 + self.name = adapter_human_name(adapter, source) if adapter != source else source + self.scanning = True + + @contextmanager + def connecting(self) -> Generator[None, None, None]: + """Context manager to track connecting state.""" + self._connecting += 1 + self.scanning = not self._connecting + try: + yield + finally: + self._connecting -= 1 + self.scanning = not self._connecting + + @property + @abstractmethod + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + + @property + @abstractmethod + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices and their advertisement data.""" + + async def async_diagnostics(self) -> dict[str, Any]: + """Return diagnostic information about the scanner.""" + return { + "type": self.__class__.__name__, + "discovered_devices_and_advertisement_data": [ + { + "name": device_adv[0].name, + "address": device_adv[0].address, + "rssi": device_adv[0].rssi, + "advertisement_data": device_adv[1], + "details": device_adv[0].details, + } + for device_adv in self.discovered_devices_and_advertisement_data.values() + ], + } + + +class BaseHaRemoteScanner(BaseHaScanner): + """Base class for a Home Assistant remote BLE scanner.""" + + __slots__ = ( + "_new_info_callback", + "_discovered_device_advertisement_datas", + "_discovered_device_timestamps", + "_connector", + "_connectable", + "_details", + "_expire_seconds", + ) + + def __init__( + self, + hass: HomeAssistant, + scanner_id: str, + name: str, + new_info_callback: Callable[[BluetoothServiceInfoBleak], None], + connector: HaBluetoothConnector, + connectable: bool, + ) -> None: + """Initialize the scanner.""" + super().__init__(hass, scanner_id, name) + self._new_info_callback = new_info_callback + self._discovered_device_advertisement_datas: dict[ + str, tuple[BLEDevice, AdvertisementData] + ] = {} + self._discovered_device_timestamps: dict[str, float] = {} + self._connector = connector + self._connectable = connectable + self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} + self._expire_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + if connectable: + self._details["connector"] = connector + self._expire_seconds = ( + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) + + @hass_callback + def async_setup(self) -> CALLBACK_TYPE: + """Set up the scanner.""" + return async_track_time_interval( + self.hass, self._async_expire_devices, timedelta(seconds=30) + ) + + def _async_expire_devices(self, _datetime: datetime.datetime) -> None: + """Expire old devices.""" + now = MONOTONIC_TIME() + expired = [ + address + for address, timestamp in self._discovered_device_timestamps.items() + if now - timestamp > self._expire_seconds + ] + for address in expired: + del self._discovered_device_advertisement_datas[address] + del self._discovered_device_timestamps[address] + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return [ + device_advertisement_data[0] + for device_advertisement_data in self._discovered_device_advertisement_datas.values() + ] + + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices and advertisement data.""" + return self._discovered_device_advertisement_datas + + @hass_callback + def _async_on_advertisement( + self, + address: str, + rssi: int, + local_name: str | None, + service_uuids: list[str], + service_data: dict[str, bytes], + manufacturer_data: dict[int, bytes], + tx_power: int | None, + details: dict[Any, Any], + ) -> None: + """Call the registered callback.""" + now = MONOTONIC_TIME() + if prev_discovery := self._discovered_device_advertisement_datas.get(address): + # Merge the new data with the old data + # to function the same as BlueZ which + # merges the dicts on PropertiesChanged + prev_device = prev_discovery[0] + prev_advertisement = prev_discovery[1] + if ( + local_name + and prev_device.name + and len(prev_device.name) > len(local_name) + ): + local_name = prev_device.name + if prev_advertisement.service_uuids: + service_uuids = list( + set(service_uuids + prev_advertisement.service_uuids) + ) + if prev_advertisement.service_data: + service_data = {**prev_advertisement.service_data, **service_data} + if prev_advertisement.manufacturer_data: + manufacturer_data = { + **prev_advertisement.manufacturer_data, + **manufacturer_data, + } + + advertisement_data = AdvertisementData( + local_name=None if local_name == "" else local_name, + manufacturer_data=manufacturer_data, + service_data=service_data, + service_uuids=service_uuids, + rssi=rssi, + tx_power=NO_RSSI_VALUE if tx_power is None else tx_power, + platform_data=(), + ) + device = BLEDevice( # type: ignore[no-untyped-call] + address=address, + name=local_name, + details=self._details | details, + rssi=rssi, # deprecated, will be removed in newer bleak + ) + self._discovered_device_advertisement_datas[address] = ( + device, + advertisement_data, + ) + self._discovered_device_timestamps[address] = now + self._new_info_callback( + BluetoothServiceInfoBleak( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=self.source, + device=device, + advertisement=advertisement_data, + connectable=self._connectable, + time=now, + ) + ) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 324520a8b5b..0a414b36144 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -3,27 +3,39 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, cast +from bluetooth_adapters import ( + ADAPTER_ADDRESS, + AdapterDetails, + adapter_human_name, + adapter_unique_name, + get_adapters, +) import voluptuous as vol from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import callback +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from homeassistant.helpers.typing import DiscoveryInfoType from . import models -from .const import ( - ADAPTER_ADDRESS, - CONF_ADAPTER, - CONF_DETAILS, - CONF_PASSIVE, - DOMAIN, - AdapterDetails, -) -from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters +from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN if TYPE_CHECKING: from homeassistant.data_entry_flow import FlowResult +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSIVE, default=False): bool, + } +) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" @@ -87,7 +99,9 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): ) configured_addresses = self._async_current_ids() - self._adapters = await async_get_bluetooth_adapters() + bluetooth_adapters = get_adapters() + await bluetooth_adapters.refresh() + self._adapters = bluetooth_adapters.adapters unconfigured_adapters = [ adapter for adapter, details in self._adapters.items() @@ -126,37 +140,12 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: ConfigEntry, - ) -> OptionsFlowHandler: + ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) @classmethod @callback def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" return bool(models.MANAGER and models.MANAGER.supports_passive_scan) - - -class OptionsFlowHandler(OptionsFlow): - """Handle the option flow for bluetooth.""" - - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_PASSIVE, - default=self.config_entry.options.get(CONF_PASSIVE, False), - ): bool, - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 038c2b1988f..150239eec02 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import Final, TypedDict +from typing import Final DOMAIN = "bluetooth" @@ -10,18 +10,6 @@ CONF_ADAPTER = "adapter" CONF_DETAILS = "details" CONF_PASSIVE = "passive" -WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth" -MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth" -UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" - -DEFAULT_ADAPTER_BY_PLATFORM = { - "Windows": WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, - "Darwin": MACOS_DEFAULT_BLUETOOTH_ADAPTER, -} - - -# Some operating systems hide the adapter address for privacy reasons (ex MacOS) -DEFAULT_ADDRESS: Final = "00:00:00:00:00:00" SOURCE_LOCAL: Final = "local" @@ -43,6 +31,15 @@ START_TIMEOUT = 15 # FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 60 * 15 +# The maximum time between advertisements for a device to be considered +# stale when the advertisement tracker can determine the interval for +# connectable devices. +# +# BlueZ uses 180 seconds by default but we give it a bit more time +# to account for the esp32's bluetooth stack being a bit slower +# than BlueZ's. +CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 195 + # We must recover before we hit the 180s mark # where the device is removed from the stack @@ -66,18 +63,3 @@ SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30) # are not present LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS = 120 BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS = 5 - - -class AdapterDetails(TypedDict, total=False): - """Adapter details.""" - - address: str - sw_version: str - hw_version: str | None - passive_scan: bool - - -ADAPTER_ADDRESS: Final = "address" -ADAPTER_SW_VERSION: Final = "sw_version" -ADAPTER_HW_VERSION: Final = "hw_version" -ADAPTER_PASSIVE_SCAN: Final = "passive_scan" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d29023acef7..534bd636355 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable -from dataclasses import replace from datetime import datetime, timedelta import itertools import logging @@ -11,6 +10,12 @@ from typing import TYPE_CHECKING, Any, Final from bleak.backends.scanner import AdvertisementDataCallback from bleak_retry_connector import NO_RSSI_VALUE, RSSI_SWITCH_THRESHOLD +from bluetooth_adapters import ( + ADAPTER_ADDRESS, + ADAPTER_PASSIVE_SCAN, + AdapterDetails, + BluetoothAdapters, +) from homeassistant import config_entries from homeassistant.core import ( @@ -24,12 +29,10 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker +from .base_scanner import BaseHaScanner from .const import ( - ADAPTER_ADDRESS, - ADAPTER_PASSIVE_SCAN, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, - AdapterDetails, ) from .match import ( ADDRESS, @@ -41,14 +44,9 @@ from .match import ( IntegrationMatcher, ble_device_matches, ) -from .models import ( - BaseHaScanner, - BluetoothCallback, - BluetoothChange, - BluetoothServiceInfoBleak, -) +from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher -from .util import async_get_bluetooth_adapters, async_load_history_from_system +from .util import async_load_history_from_system if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -103,6 +101,7 @@ class BluetoothManager: self, hass: HomeAssistant, integration_matcher: IntegrationMatcher, + bluetooth_adapters: BluetoothAdapters, ) -> None: """Init bluetooth manager.""" self.hass = hass @@ -127,7 +126,8 @@ class BluetoothManager: self._non_connectable_scanners: list[BaseHaScanner] = [] self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} - self._sources: set[str] = set() + self._sources: dict[str, BaseHaScanner] = {} + self._bluetooth_adapters = bluetooth_adapters @property def supports_passive_scan(self) -> bool: @@ -169,25 +169,34 @@ class BluetoothManager: return adapter return None + @hass_callback + def async_scanner_by_source(self, source: str) -> BaseHaScanner | None: + """Return the scanner for a source.""" + return self._sources.get(source) + async def async_get_bluetooth_adapters( self, cached: bool = True ) -> dict[str, AdapterDetails]: """Get bluetooth adapters.""" - if not cached or not self._adapters: - self._adapters = await async_get_bluetooth_adapters() + if not self._adapters or not cached: + if not cached: + await self._bluetooth_adapters.refresh() + self._adapters = self._bluetooth_adapters.adapters return self._adapters async def async_get_adapter_from_address(self, address: str) -> str | None: """Get adapter from address.""" if adapter := self._find_adapter_by_address(address): return adapter - self._adapters = await async_get_bluetooth_adapters() + await self._bluetooth_adapters.refresh() + self._adapters = self._bluetooth_adapters.adapters return self._find_adapter_by_address(address) async def async_setup(self) -> None: """Set up the bluetooth manager.""" + await self._bluetooth_adapters.refresh() install_multiple_bleak_catcher() - history = await async_load_history_from_system() + history = async_load_history_from_system(self._bluetooth_adapters) # Everything is connectable so it fall into both # buckets since the host system can only provide # connectable devices @@ -298,6 +307,7 @@ class BluetoothManager: self, old: BluetoothServiceInfoBleak, new: BluetoothServiceInfoBleak, + debug: bool, ) -> bool: """Prefer previous advertisement from a different source if it is better.""" if new.time - old.time > ( @@ -306,34 +316,32 @@ class BluetoothManager: ) ): # If the old advertisement is stale, any new advertisement is preferred - _LOGGER.debug( - "%s (%s): Switching from %s[%s] to %s[%s] (time elapsed:%s > stale seconds:%s)", - new.name, - new.address, - old.source, - old.connectable, - new.source, - new.connectable, - new.time - old.time, - stale_seconds, - ) + if debug: + _LOGGER.debug( + "%s (%s): Switching from %s to %s (time elapsed:%s > stale seconds:%s)", + new.name, + new.address, + self._async_describe_source(old), + self._async_describe_source(new), + new.time - old.time, + stale_seconds, + ) return False if (new.rssi or NO_RSSI_VALUE) - RSSI_SWITCH_THRESHOLD > ( old.rssi or NO_RSSI_VALUE ): # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred - _LOGGER.debug( - "%s (%s): Switching from %s[%s] to %s[%s] (new rssi:%s - threshold:%s > old rssi:%s)", - new.name, - new.address, - old.source, - old.connectable, - new.source, - new.connectable, - new.rssi, - RSSI_SWITCH_THRESHOLD, - old.rssi, - ) + if debug: + _LOGGER.debug( + "%s (%s): Switching from %s to %s (new rssi:%s - threshold:%s > old rssi:%s)", + new.name, + new.address, + self._async_describe_source(old), + self._async_describe_source(new), + new.rssi, + RSSI_SWITCH_THRESHOLD, + old.rssi, + ) return False return True @@ -363,6 +371,7 @@ class BluetoothManager: connectable_history = self._connectable_history source = service_info.source + debug = _LOGGER.isEnabledFor(logging.DEBUG) # This logic is complex due to the many combinations of scanners that are supported. # # We need to handle multiple connectable and non-connectable scanners @@ -380,9 +389,10 @@ class BluetoothManager: if ( (old_service_info := all_history.get(address)) and source != old_service_info.source - and old_service_info.source in self._sources + and (scanner := self._sources.get(old_service_info.source)) + and scanner.scanning and self._prefer_previous_adv_from_different_source( - old_service_info, service_info + old_service_info, service_info, debug ) ): # If we are rejecting the new advertisement and the device is connectable @@ -400,9 +410,14 @@ class BluetoothManager: # the old connectable advertisement or ( source != old_connectable_service_info.source - and old_connectable_service_info.source in self._sources + and ( + connectable_scanner := self._sources.get( + old_connectable_service_info.source + ) + ) + and connectable_scanner.scanning and self._prefer_previous_adv_from_different_source( - old_connectable_service_info, service_info + old_connectable_service_info, service_info, debug ) ) ): @@ -442,18 +457,29 @@ class BluetoothManager: # route any connection attempts to the connectable path, we # mark the service_info as connectable so that the callbacks # will be called and the device can be discovered. - service_info = replace(service_info, connectable=True) + service_info = BluetoothServiceInfoBleak( + name=service_info.name, + address=service_info.address, + rssi=service_info.rssi, + manufacturer_data=service_info.manufacturer_data, + service_data=service_info.service_data, + service_uuids=service_info.service_uuids, + source=service_info.source, + device=service_info.device, + advertisement=service_info.advertisement, + connectable=True, + time=service_info.time, + ) matched_domains = self._integration_matcher.match_domains(service_info) - _LOGGER.debug( - "%s: %s %s connectable: %s match: %s rssi: %s", - source, - address, - advertisement_data, - connectable, - matched_domains, - advertisement_data.rssi, - ) + if debug: + _LOGGER.debug( + "%s: %s %s match: %s", + self._async_describe_source(service_info), + address, + advertisement_data, + matched_domains, + ) if is_connectable_by_any_source: # Bleak callbacks must get a connectable device @@ -475,6 +501,17 @@ class BluetoothManager: service_info, ) + @hass_callback + def _async_describe_source(self, service_info: BluetoothServiceInfoBleak) -> str: + """Describe a source.""" + if scanner := self._sources.get(service_info.source): + description = scanner.name + else: + description = service_info.source + if service_info.connectable: + description += " [connectable]" + return description + @hass_callback def async_track_unavailable( self, @@ -595,15 +632,17 @@ class BluetoothManager: self, scanner: BaseHaScanner, connectable: bool ) -> CALLBACK_TYPE: """Register a new scanner.""" + _LOGGER.debug("Registering scanner %s", scanner.name) scanners = self._get_scanners_by_type(connectable) def _unregister_scanner() -> None: + _LOGGER.debug("Unregistering scanner %s", scanner.name) self._advertisement_tracker.async_remove_source(scanner.source) scanners.remove(scanner) - self._sources.remove(scanner.source) + del self._sources[scanner.source] scanners.append(scanner) - self._sources.add(scanner.source) + self._sources[scanner.source] = scanner return _unregister_scanner @hass_callback diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 5e56d125f44..a8e1c02c8b0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,12 +7,19 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.8.5", - "bluetooth-adapters==0.7.0", - "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.61.1" + "bleak-retry-connector==2.10.1", + "bluetooth-adapters==0.12.0", + "bluetooth-auto-recovery==0.5.4", + "bluetooth-data-tools==0.3.0", + "dbus-fast==1.75.0" ], "codeowners": ["@bdraco"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": [ + "btsocket", + "bleak_retry_connector", + "bluetooth_adapters", + "bluetooth_auto_recovery" + ] } diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a63a704baf6..58067a467ce 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -1,74 +1,33 @@ """Models for bluetooth.""" from __future__ import annotations -from abc import abstractmethod -import asyncio from collections.abc import Callable -import contextlib from dataclasses import dataclass from enum import Enum -import logging -from typing import TYPE_CHECKING, Any, Final +from typing import TYPE_CHECKING, Final -from bleak import BleakClient, BleakError -from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import ( - AdvertisementData, - AdvertisementDataCallback, - BaseBleakScanner, -) -from bleak_retry_connector import NO_RSSI_VALUE, freshen_ble_device +from bleak import BaseBleakClient +from home_assistant_bluetooth import BluetoothServiceInfoBleak -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback -from homeassistant.helpers.frame import report -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.util.dt import monotonic_time_coarse if TYPE_CHECKING: from .manager import BluetoothManager -_LOGGER = logging.getLogger(__name__) - -FILTER_UUIDS: Final = "UUIDs" - MANAGER: BluetoothManager | None = None +MONOTONIC_TIME: Final = monotonic_time_coarse + @dataclass -class BluetoothServiceInfoBleak(BluetoothServiceInfo): - """BluetoothServiceInfo with bleak data. +class HaBluetoothConnector: + """Data for how to connect a BLEDevice from a given scanner.""" - Integrations may need BLEDevice and AdvertisementData - to connect to the device without having bleak trigger - another scan to translate the address to the system's - internal details. - """ - - device: BLEDevice - advertisement: AdvertisementData - connectable: bool - time: float - - def as_dict(self) -> dict[str, Any]: - """Return as dict. - - The dataclass asdict method is not used because - it will try to deepcopy pyobjc data which will fail. - """ - return { - "name": self.name, - "address": self.address, - "rssi": self.rssi, - "manufacturer_data": self.manufacturer_data, - "service_data": self.service_data, - "service_uuids": self.service_uuids, - "source": self.source, - "advertisement": self.advertisement, - "connectable": self.connectable, - "time": self.time, - } + client: type[BaseBleakClient] + source: str + can_connect: Callable[[], bool] class BluetoothScanningMode(Enum): @@ -81,298 +40,3 @@ class BluetoothScanningMode(Enum): BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] - - -@dataclass -class HaBluetoothConnector: - """Data for how to connect a BLEDevice from a given scanner.""" - - client: type[BaseBleakClient] - source: str - can_connect: Callable[[], bool] - - -@dataclass -class _HaWrappedBleakBackend: - """Wrap bleak backend to make it usable by Home Assistant.""" - - device: BLEDevice - client: type[BaseBleakClient] - - -class BaseHaScanner: - """Base class for Ha Scanners.""" - - def __init__(self, hass: HomeAssistant, source: str) -> None: - """Initialize the scanner.""" - self.hass = hass - self.source = source - - @property - @abstractmethod - def discovered_devices(self) -> list[BLEDevice]: - """Return a list of discovered devices.""" - - @property - @abstractmethod - def discovered_devices_and_advertisement_data( - self, - ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: - """Return a list of discovered devices and their advertisement data.""" - - async def async_diagnostics(self) -> dict[str, Any]: - """Return diagnostic information about the scanner.""" - return { - "type": self.__class__.__name__, - "discovered_devices": [ - { - "name": device.name, - "address": device.address, - } - for device in self.discovered_devices - ], - } - - -class HaBleakScannerWrapper(BaseBleakScanner): - """A wrapper that uses the single instance.""" - - def __init__( - self, - *args: Any, - detection_callback: AdvertisementDataCallback | None = None, - service_uuids: list[str] | None = None, - **kwargs: Any, - ) -> None: - """Initialize the BleakScanner.""" - self._detection_cancel: CALLBACK_TYPE | None = None - self._mapped_filters: dict[str, set[str]] = {} - self._advertisement_data_callback: AdvertisementDataCallback | None = None - remapped_kwargs = { - "detection_callback": detection_callback, - "service_uuids": service_uuids or [], - **kwargs, - } - self._map_filters(*args, **remapped_kwargs) - super().__init__( - detection_callback=detection_callback, service_uuids=service_uuids or [] - ) - - @classmethod - async def discover(cls, timeout: float = 5.0, **kwargs: Any) -> list[BLEDevice]: - """Discover devices.""" - assert MANAGER is not None - return list(MANAGER.async_discovered_devices(True)) - - async def stop(self, *args: Any, **kwargs: Any) -> None: - """Stop scanning for devices.""" - - async def start(self, *args: Any, **kwargs: Any) -> None: - """Start scanning for devices.""" - - def _map_filters(self, *args: Any, **kwargs: Any) -> bool: - """Map the filters.""" - mapped_filters = {} - if filters := kwargs.get("filters"): - if filter_uuids := filters.get(FILTER_UUIDS): - mapped_filters[FILTER_UUIDS] = set(filter_uuids) - else: - _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) - if service_uuids := kwargs.get("service_uuids"): - mapped_filters[FILTER_UUIDS] = set(service_uuids) - if mapped_filters == self._mapped_filters: - return False - self._mapped_filters = mapped_filters - return True - - def set_scanning_filter(self, *args: Any, **kwargs: Any) -> None: - """Set the filters to use.""" - if self._map_filters(*args, **kwargs): - self._setup_detection_callback() - - def _cancel_callback(self) -> None: - """Cancel callback.""" - if self._detection_cancel: - self._detection_cancel() - self._detection_cancel = None - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return a list of discovered devices.""" - assert MANAGER is not None - return list(MANAGER.async_discovered_devices(True)) - - def register_detection_callback( - self, callback: AdvertisementDataCallback | None - ) -> None: - """Register a callback that is called when a device is discovered or has a property changed. - - This method takes the callback and registers it with the long running - scanner. - """ - self._advertisement_data_callback = callback - self._setup_detection_callback() - - def _setup_detection_callback(self) -> None: - """Set up the detection callback.""" - if self._advertisement_data_callback is None: - return - self._cancel_callback() - super().register_detection_callback(self._advertisement_data_callback) - assert MANAGER is not None - assert self._callback is not None - self._detection_cancel = MANAGER.async_register_bleak_callback( - self._callback, self._mapped_filters - ) - - def __del__(self) -> None: - """Delete the BleakScanner.""" - if self._detection_cancel: - # Nothing to do if event loop is already closed - with contextlib.suppress(RuntimeError): - asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) - - -class HaBleakClientWrapper(BleakClient): - """Wrap the BleakClient to ensure it does not shutdown our scanner. - - If an address is passed into BleakClient instead of a BLEDevice, - bleak will quietly start a new scanner under the hood to resolve - the address. This can cause a conflict with our scanner. We need - to handle translating the address to the BLEDevice in this case - to avoid the whole stack from getting stuck in an in progress state - when an integration does this. - """ - - def __init__( # pylint: disable=super-init-not-called, keyword-arg-before-vararg - self, - address_or_ble_device: str | BLEDevice, - disconnected_callback: Callable[[BleakClient], None] | None = None, - *args: Any, - timeout: float = 10.0, - **kwargs: Any, - ) -> None: - """Initialize the BleakClient.""" - if isinstance(address_or_ble_device, BLEDevice): - self.__address = address_or_ble_device.address - else: - report( - "attempted to call BleakClient with an address instead of a BLEDevice", - exclude_integrations={"bluetooth"}, - error_if_core=False, - ) - self.__address = address_or_ble_device - self.__disconnected_callback = disconnected_callback - self.__timeout = timeout - self.__ble_device: BLEDevice | None = None - self._backend: BaseBleakClient | None = None # type: ignore[assignment] - - @property - def is_connected(self) -> bool: - """Return True if the client is connected to a device.""" - return self._backend is not None and self._backend.is_connected - - def set_disconnected_callback( - self, - callback: Callable[[BleakClient], None] | None, - **kwargs: Any, - ) -> None: - """Set the disconnect callback.""" - self.__disconnected_callback = callback - if self._backend: - self._backend.set_disconnected_callback(callback, **kwargs) # type: ignore[arg-type] - - async def connect(self, **kwargs: Any) -> bool: - """Connect to the specified GATT server.""" - if ( - not self._backend - or not self.__ble_device - or not self._async_get_backend_for_ble_device(self.__ble_device) - ): - assert MANAGER is not None - wrapped_backend = ( - self._async_get_backend() or self._async_get_fallback_backend() - ) - self.__ble_device = ( - await freshen_ble_device(wrapped_backend.device) - or wrapped_backend.device - ) - self._backend = wrapped_backend.client( - self.__ble_device, - disconnected_callback=self.__disconnected_callback, - timeout=self.__timeout, - hass=MANAGER.hass, - ) - return await super().connect(**kwargs) - - @hass_callback - def _async_get_backend_for_ble_device( - self, ble_device: BLEDevice - ) -> _HaWrappedBleakBackend | None: - """Get the backend for a BLEDevice.""" - details = ble_device.details - if not isinstance(details, dict) or "connector" not in details: - # If client is not defined in details - # its the client for this platform - cls = get_platform_client_backend_type() - return _HaWrappedBleakBackend(ble_device, cls) - - connector: HaBluetoothConnector = details["connector"] - # Make sure the backend can connect to the device - # as some backends have connection limits - if not connector.can_connect(): - return None - - return _HaWrappedBleakBackend(ble_device, connector.client) - - @hass_callback - def _async_get_backend(self) -> _HaWrappedBleakBackend | None: - """Get the bleak backend for the given address.""" - assert MANAGER is not None - address = self.__address - ble_device = MANAGER.async_ble_device_from_address(address, True) - if ble_device is None: - raise BleakError(f"No device found for address {address}") - - if backend := self._async_get_backend_for_ble_device(ble_device): - return backend - - return None - - @hass_callback - def _async_get_fallback_backend(self) -> _HaWrappedBleakBackend: - """Get a fallback backend for the given address.""" - # - # The preferred backend cannot currently connect the device - # because it is likely out of connection slots. - # - # We need to try all backends to find one that can - # connect to the device. - # - assert MANAGER is not None - address = self.__address - device_advertisement_datas = ( - MANAGER.async_get_discovered_devices_and_advertisement_data_by_address( - address, True - ) - ) - for device_advertisement_data in sorted( - device_advertisement_datas, - key=lambda device_advertisement_data: device_advertisement_data[1].rssi - or NO_RSSI_VALUE, - reverse=True, - ): - if backend := self._async_get_backend_for_ble_device( - device_advertisement_data[0] - ): - return backend - - raise BleakError( - f"No backend with an available connection slot that can reach address {address} was found" - ) - - async def disconnect(self) -> bool: - """Disconnect from the device.""" - if self._backend is None: - return True - return await self._backend.disconnect() diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 6b23cae0218..09032715c74 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -16,6 +16,8 @@ from bleak.backends.bluezdbus.advertisement_monitor import OrPattern from bleak.backends.bluezdbus.scanner import BlueZScannerArgs from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback +from bleak_retry_connector import restore_discoveries +from bluetooth_adapters import DEFAULT_ADDRESS from dbus_fast import InvalidMessageError from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback @@ -24,15 +26,15 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import monotonic_time_coarse from homeassistant.util.package import is_docker_env +from .base_scanner import BaseHaScanner from .const import ( - DEFAULT_ADDRESS, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, START_TIMEOUT, ) -from .models import BaseHaScanner, BluetoothScanningMode, BluetoothServiceInfoBleak -from .util import adapter_human_name, async_reset_adapter +from .models import BluetoothScanningMode, BluetoothServiceInfoBleak +from .util import async_reset_adapter OriginalBleakScanner = bleak.BleakScanner MONOTONIC_TIME = monotonic_time_coarse @@ -125,18 +127,19 @@ class HaScanner(BaseHaScanner): mode: BluetoothScanningMode, adapter: str, address: str, + new_info_callback: Callable[[BluetoothServiceInfoBleak], None], ) -> None: """Init bluetooth discovery.""" source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL - super().__init__(hass, source) + super().__init__(hass, source, adapter) self.mode = mode self.adapter = adapter self._start_stop_lock = asyncio.Lock() self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 self._start_time = 0.0 - self._callbacks: list[Callable[[BluetoothServiceInfoBleak], None]] = [] - self.name = adapter_human_name(adapter, address) + self._new_info_callback = new_info_callback + self.scanning = False @property def discovered_devices(self) -> list[BLEDevice]: @@ -168,22 +171,6 @@ class HaScanner(BaseHaScanner): "start_time": self._start_time, } - @hass_callback - def async_register_callback( - self, callback: Callable[[BluetoothServiceInfoBleak], None] - ) -> CALLBACK_TYPE: - """Register a callback. - - Currently this is used to feed the callbacks into the - central manager. - """ - - def _remove() -> None: - self._callbacks.remove(callback) - - self._callbacks.append(callback) - return _remove - @hass_callback def _async_detection_callback( self, @@ -206,21 +193,21 @@ class HaScanner(BaseHaScanner): # as the adapter is in a failure # state if all the data is empty. self._last_detection = callback_time - service_info = BluetoothServiceInfoBleak( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=self.source, - device=device, - advertisement=advertisement_data, - connectable=True, - time=callback_time, + self._new_info_callback( + BluetoothServiceInfoBleak( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=advertisement_data.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=self.source, + device=device, + advertisement=advertisement_data, + connectable=True, + time=callback_time, + ) ) - for callback in self._callbacks: - callback(service_info) async def async_start(self) -> None: """Start bluetooth scanner.""" @@ -326,7 +313,9 @@ class HaScanner(BaseHaScanner): # Everything is fine, break out of the loop break + self.scanning = True self._async_setup_scanner_watchdog() + await restore_discoveries(self.scanner, self.adapter) @hass_callback def _async_setup_scanner_watchdog(self) -> None: @@ -399,6 +388,7 @@ class HaScanner(BaseHaScanner): async def _async_stop_scanner(self) -> None: """Stop bluetooth discovery under the lock.""" + self.scanning = False _LOGGER.debug("%s: Stopping bluetooth discovery", self.name) try: await self.scanner.stop() # type: ignore[no-untyped-call] diff --git a/homeassistant/components/bluetooth/translations/bg.json b/homeassistant/components/bluetooth/translations/bg.json index 7da387cae4c..6bcfe5a8b87 100644 --- a/homeassistant/components/bluetooth/translations/bg.json +++ b/homeassistant/components/bluetooth/translations/bg.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, - "enable_bluetooth": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" @@ -38,7 +35,6 @@ "step": { "init": { "data": { - "adapter": "Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440, \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435", "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435" } } diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json index 8c124446672..b7e3bc3eea3 100644 --- a/homeassistant/components/bluetooth/translations/ca.json +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Vols configurar {name}?" }, - "enable_bluetooth": { - "description": "Vols configurar Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adaptador" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Adaptador Bluetooth a utilitzar per escanejar", "passive": "Escaneig passiu" - }, - "description": "L'escolta passiva necessita BlueZ 5.63 o posterior i les funcions experimentals activades." + } } } } diff --git a/homeassistant/components/bluetooth/translations/cs.json b/homeassistant/components/bluetooth/translations/cs.json index e53690d3458..005d948fbcf 100644 --- a/homeassistant/components/bluetooth/translations/cs.json +++ b/homeassistant/components/bluetooth/translations/cs.json @@ -5,9 +5,6 @@ "bluetooth_confirm": { "description": "Chcete nastavit {name}?" }, - "enable_bluetooth": { - "description": "Chcete nastavit Bluetooth?" - }, "single_adapter": { "description": "Chcete nastavit Bluetooth adapt\u00e9r {name}?" }, diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 63bbf51c59e..6c7a5c15139 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, - "enable_bluetooth": { - "description": "M\u00f6chtest du Bluetooth einrichten?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -31,7 +28,7 @@ }, "issues": { "haos_outdated": { - "description": "Zur Verbesserung der Bluetooth-Zuverl\u00e4ssigkeit und -Leistung empfehlen wir dir dringend ein Update auf Version 9.0 oder h\u00f6her des Home Assistant-Betriebssystems.", + "description": "Zur Verbesserung der Bluetooth Zuverl\u00e4ssigkeit und Leistung empfehlen wir dir dringend ein Update auf Version 9.0 oder h\u00f6her des Home Assistant-Betriebssystems.", "title": "Aktualisiere auf das Home Assistant-Betriebssystem 9.0 oder h\u00f6her" } }, @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Der zum Scannen zu verwendende Bluetooth-Adapter", "passive": "Passives Scannen" - }, - "description": "Passives Mith\u00f6ren erfordert BlueZ 5.63 oder h\u00f6her mit aktivierten experimentellen Funktionen." + } } } } diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index f31e930b3fa..0e0e512e9a5 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, - "enable_bluetooth": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Bluetooth;" - }, "multiple_adapters": { "data": { "adapter": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "\u039f \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", "passive": "\u03a0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7" - }, - "description": "\u0397 \u03c0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af BlueZ 5.63 \u03ae \u03bc\u03b5\u03c4\u03b1\u03b3\u03b5\u03bd\u03ad\u03c3\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03bc\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03c0\u03b5\u03b9\u03c1\u03b1\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b5\u03c2." + } } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 73ed74356fd..beefb842204 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "The Bluetooth Adapter to use for scanning", "passive": "Passive scanning" - }, - "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." + } } } } diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index 4cfa38df9c2..f72348c4794 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "\u00bfQuieres configurar {name}?" }, - "enable_bluetooth": { - "description": "\u00bfQuieres configurar Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adaptador" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "El adaptador Bluetooth que se usar\u00e1 para escanear", "passive": "Escaneo pasivo" - }, - "description": "La escucha pasiva requiere BlueZ 5.63 o posterior con funciones experimentales habilitadas." + } } } } diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json index 5579ab5b62d..d6413e23648 100644 --- a/homeassistant/components/bluetooth/translations/et.json +++ b/homeassistant/components/bluetooth/translations/et.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Kas seadistada {name} ?" }, - "enable_bluetooth": { - "description": "Kas soovid Bluetoothi seadistada?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Sk\u00e4nnimiseks kasutatav Bluetoothi adapter", "passive": "Passiivne sk\u00e4nnimine" - }, - "description": "Passiivseks kuulamiseks on vaja BlueZ 5.63 v\u00f5i uuemat versiooni koos lubatud eksperimentaalsete funktsioonidega." + } } } } diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json index c7a1155b216..03cc6857910 100644 --- a/homeassistant/components/bluetooth/translations/fr.json +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -8,15 +8,15 @@ "bluetooth_confirm": { "description": "Voulez-vous configurer {name}\u00a0?" }, - "enable_bluetooth": { - "description": "Voulez-vous configurer le Bluetooth\u00a0?" - }, "multiple_adapters": { "data": { "adapter": "Adaptateur" }, "description": "S\u00e9lectionner un adaptateur Bluetooth \u00e0 configurer" }, + "single_adapter": { + "description": "Voulez-vous configurer l\u2019adaptateur Bluetooth {name}\u00a0?" + }, "user": { "data": { "address": "Appareil" @@ -25,13 +25,18 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Pour am\u00e9liorer la fiabilit\u00e9 et les performances du Bluetooth, nous vous recommandons vivement de passer \u00e0 la version 9.0 ou ult\u00e9rieure du syst\u00e8me d'exploitation d'Home Assistant.", + "title": "Mettez \u00e0 jour le syst\u00e8me d'exploitation Home Assistant (HAOS) vers la version 9.0 ou sup\u00e9rieure" + } + }, "options": { "step": { "init": { "data": { "passive": "Recherche passive" - }, - "description": "L'\u00e9coute passive n\u00e9cessite BlueZ version 5.63 ou ult\u00e9rieure avec les fonctions exp\u00e9rimentales activ\u00e9es." + } } } } diff --git a/homeassistant/components/bluetooth/translations/he.json b/homeassistant/components/bluetooth/translations/he.json index ad9bb727c62..b2cb1c6814d 100644 --- a/homeassistant/components/bluetooth/translations/he.json +++ b/homeassistant/components/bluetooth/translations/he.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" }, - "enable_bluetooth": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4?" - }, "multiple_adapters": { "data": { "adapter": "\u05de\u05ea\u05d0\u05dd" @@ -29,14 +26,18 @@ } } }, + "issues": { + "haos_outdated": { + "description": "\u05db\u05d3\u05d9 \u05dc\u05e9\u05e4\u05e8 \u05d0\u05ea \u05d4\u05de\u05d4\u05d9\u05de\u05e0\u05d5\u05ea \u05d5\u05d4\u05d1\u05d9\u05e6\u05d5\u05e2\u05d9\u05dd \u05e9\u05dc Bluetooth, \u05de\u05d5\u05de\u05dc\u05e5 \u05de\u05d0\u05d5\u05d3 \u05dc\u05e2\u05d3\u05db\u05df \u05dc\u05d2\u05e8\u05e1\u05d4 9.0 \u05d5\u05d0\u05d9\u05dc\u05da \u05e9\u05dc \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d4\u05e4\u05e2\u05dc\u05d4 Home Assistant.", + "title": "\u05e2\u05d3\u05db\u05d5\u05df \u05dc\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d4\u05e4\u05e2\u05dc\u05d4 Home Assistant 9.0 \u05d5\u05d0\u05d9\u05dc\u05da" + } + }, "options": { "step": { "init": { "data": { - "adapter": "\u05de\u05ea\u05d0\u05dd \u05d4\u05e9\u05df \u05d4\u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", "passive": "\u05e1\u05e8\u05d9\u05e7\u05d4 \u05e4\u05e1\u05d9\u05d1\u05d9\u05ea" - }, - "description": "\u05d4\u05d0\u05d6\u05e0\u05d4 \u05e4\u05e1\u05d9\u05d1\u05d9\u05ea \u05d3\u05d5\u05e8\u05e9\u05ea BlueZ 5.63 \u05d5\u05d0\u05d9\u05dc\u05da \u05e2\u05dd \u05ea\u05db\u05d5\u05e0\u05d5\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05e0\u05d9\u05d5\u05ea \u05de\u05d5\u05e4\u05e2\u05dc\u05d5\u05ea." + } } } } diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index e5b94f070ea..85060552cb1 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -2,16 +2,13 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "no_adapters": "Nem tal\u00e1ltak konfigur\u00e1latlan Bluetooth-adaptert" + "no_adapters": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan Bluetooth-adapter" }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, - "enable_bluetooth": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Bluetooth-ot?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "A szkennel\u00e9shez haszn\u00e1lhat\u00f3 Bluetooth-adapter", "passive": "Passz\u00edv figyel\u00e9s" - }, - "description": "A passz\u00edv hallgat\u00e1shoz BlueZ 5.63 vagy \u00fajabb verzi\u00f3ra van sz\u00fcks\u00e9g, a k\u00eds\u00e9rleti funkci\u00f3k enged\u00e9lyez\u00e9s\u00e9vel." + } } } } diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 282071fc4f1..0c049279b9c 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Ingin menyiapkan {name}?" }, - "enable_bluetooth": { - "description": "Ingin menyiapkan Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adaptor" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Adaptor Bluetooth yang digunakan untuk pemindaian", "passive": "Memindai secara pasif" - }, - "description": "Mendengarkan secara pasif memerlukan BlueZ 5.63 atau lebih baru dengan fitur eksperimental yang diaktifkan." + } } } } diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index 6d3adac2f76..4252a84c889 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Vuoi configurare {name}?" }, - "enable_bluetooth": { - "description": "Vuoi configurare il Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adattatore" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "L'adattatore Bluetooth da utilizzare per la scansione", "passive": "Scansione passiva" - }, - "description": "L'ascolto passivo richiede BlueZ 5.63 o successivo con funzionalit\u00e0 sperimentali abilitate." + } } } } diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index e19ee5b7dd2..97d77df507b 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, - "enable_bluetooth": { - "description": "Bluetooth\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" - }, "multiple_adapters": { "data": { "adapter": "\u30a2\u30c0\u30d7\u30bf\u30fc" @@ -33,10 +30,8 @@ "step": { "init": { "data": { - "adapter": "\u30b9\u30ad\u30e3\u30f3\u306b\u4f7f\u7528\u3059\u308bBluetooth\u30a2\u30c0\u30d7\u30bf\u30fc", "passive": "\u30d1\u30c3\u30b7\u30d6\u30b9\u30ad\u30e3\u30f3" - }, - "description": "\u30d1\u30c3\u30b7\u30d6 \u30ea\u30b9\u30cb\u30f3\u30b0\u306b\u306f\u3001\u5b9f\u9a13\u7684\u306a\u6a5f\u80fd\u3092\u6709\u52b9\u306b\u3057\u305f\u3001BlueZ 5.63\u4ee5\u964d\u304c\u5fc5\u8981\u3067\u3059\u3002" + } } } } diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json index 22ec504f244..3fee489370e 100644 --- a/homeassistant/components/bluetooth/translations/nl.json +++ b/homeassistant/components/bluetooth/translations/nl.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Wilt u {name} instellen?" }, - "enable_bluetooth": { - "description": "Wilt u Bluetooth instellen?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -29,14 +26,5 @@ "haos_outdated": { "title": "Update naar het Home Assistant-besturingssysteem versie 9.0 of hoger" } - }, - "options": { - "step": { - "init": { - "data": { - "adapter": "De Bluetooth-adapter die gebruikt moet worden voor het scannen." - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json index 687b651eaca..2246f72a748 100644 --- a/homeassistant/components/bluetooth/translations/no.json +++ b/homeassistant/components/bluetooth/translations/no.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Vil du konfigurere {name}?" }, - "enable_bluetooth": { - "description": "Vil du konfigurere Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Bluetooth-adapteren som skal brukes til skanning", "passive": "Passiv skanning" - }, - "description": "Passiv lytting krever BlueZ 5.63 eller nyere med eksperimentelle funksjoner aktivert." + } } } } diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 2de99ab69fe..286457bf3b2 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, - "enable_bluetooth": { - "description": "Czy chcesz skonfigurowa\u0107 Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Adapter Bluetooth u\u017cywany do skanowania", "passive": "Skanowanie pasywne" - }, - "description": "Nas\u0142uchiwanie pasywne wymaga BlueZ 5.63 lub nowszego z w\u0142\u0105czonymi funkcjami eksperymentalnymi." + } } } } diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 389205c20db..3a18865e33c 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Deseja configurar {name}?" }, - "enable_bluetooth": { - "description": "Deseja configurar o Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adaptador" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "O adaptador Bluetooth a ser usado para escaneamento", "passive": "Varredura passiva" - }, - "description": "A escuta passiva requer BlueZ 5.63 ou posterior com recursos experimentais ativados." + } } } } diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 1be63589bb4..f2cd2e000a6 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, - "enable_bluetooth": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u0414\u043b\u044f \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f BlueZ 5.63 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u044f\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0441 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u043c\u0438 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u043c\u0438." + } } } } diff --git a/homeassistant/components/bluetooth/translations/sk.json b/homeassistant/components/bluetooth/translations/sk.json new file mode 100644 index 00000000000..bbb745622ac --- /dev/null +++ b/homeassistant/components/bluetooth/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "no_adapters": "Nena\u0161li sa \u017eiadne nenakonfigurovan\u00e9 adapt\u00e9ry Bluetooth" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "multiple_adapters": { + "data": { + "adapter": "Adapt\u00e9r" + }, + "description": "Vyberte adapt\u00e9r Bluetooth, ktor\u00fd chcete nastavi\u0165" + }, + "single_adapter": { + "description": "Chcete nastavi\u0165 adapt\u00e9r Bluetooth {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + }, + "issues": { + "haos_outdated": { + "title": "Aktualizujte na opera\u010dn\u00fd syst\u00e9m Home Assistant 9.0 alebo nov\u0161\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/sv.json b/homeassistant/components/bluetooth/translations/sv.json index ee2d433fc1c..1dd27b29042 100644 --- a/homeassistant/components/bluetooth/translations/sv.json +++ b/homeassistant/components/bluetooth/translations/sv.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Vill du konfigurera {name}?" }, - "enable_bluetooth": { - "description": "Vill du s\u00e4tta upp Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Bluetooth-adaptern som ska anv\u00e4ndas f\u00f6r skanning", "passive": "Passiv skanning" - }, - "description": "Passiv lyssning kr\u00e4ver BlueZ 5.63 eller senare med experimentella funktioner aktiverade." + } } } } diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index 787bc1fed94..9e8516ba25c 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "{name} kurulumunu yapmak istiyor musunuz?" }, - "enable_bluetooth": { - "description": "Bluetooth'u kurmak istiyor musunuz?" - }, "multiple_adapters": { "data": { "adapter": "Adapt\u00f6r" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "Tarama i\u00e7in kullan\u0131lacak Bluetooth Adapt\u00f6r\u00fc", "passive": "Pasif tarama" - }, - "description": "Pasif dinleme, BlueZ 5.63 veya daha yenisini ve deneysel \u00f6zelliklerin etkinle\u015ftirilmesini gerektirir." + } } } } diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index a45ccc52d44..bd554d76b3c 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, - "enable_bluetooth": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u7259\uff1f" - }, "multiple_adapters": { "data": { "adapter": "\u50b3\u8f38\u5668" @@ -39,10 +36,8 @@ "step": { "init": { "data": { - "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u7259\u50b3\u8f38\u5668", "passive": "\u88ab\u52d5\u6383\u63cf" - }, - "description": "\u88ab\u52d5\u76e3\u807d\u9700\u8981 BlueZ 5.63 \u6216\u66f4\u65b0\u7248\u672c\u3001\u4e26\u958b\u555f\u5be6\u9a57\u529f\u80fd\u3002" + } } } } diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index 2c99f189852..a02e601a878 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import abstractmethod import logging +from typing import cast from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -69,7 +70,7 @@ class BasePassiveBluetoothCoordinator: if service_info := async_last_service_info( self.hass, self.address, self.connectable ): - return service_info.name + return cast(str, service_info.name) # for compat this can be a pyobjc return self._last_name @property diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index ba174f0306a..0b1e615ddda 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -6,7 +6,7 @@ import bleak from bleak.backends.service import BleakGATTServiceCollection import bleak_retry_connector -from .models import HaBleakClientWrapper, HaBleakScannerWrapper +from .wrappers import HaBleakClientWrapper, HaBleakScannerWrapper ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner ORIGINAL_BLEAK_CLIENT = bleak.BleakClient diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 181796d3d2d..c2336dd7af0 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -1,34 +1,20 @@ """The bluetooth integration utilities.""" from __future__ import annotations -import platform - +from bluetooth_adapters import BluetoothAdapters from bluetooth_auto_recovery import recover_adapter from homeassistant.core import callback from homeassistant.util.dt import monotonic_time_coarse -from .const import ( - DEFAULT_ADAPTER_BY_PLATFORM, - DEFAULT_ADDRESS, - MACOS_DEFAULT_BLUETOOTH_ADAPTER, - UNIX_DEFAULT_BLUETOOTH_ADAPTER, - WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, - AdapterDetails, -) from .models import BluetoothServiceInfoBleak -async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBleak]: +@callback +def async_load_history_from_system( + adapters: BluetoothAdapters, +) -> dict[str, BluetoothServiceInfoBleak]: """Load the device and advertisement_data history if available on the current system.""" - if platform.system() != "Linux": - return {} - from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel - BlueZDBusObjects, - ) - - bluez_dbus = BlueZDBusObjects() - await bluez_dbus.load() now = monotonic_time_coarse() return { address: BluetoothServiceInfoBleak( @@ -36,7 +22,7 @@ async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBlea or history.device.name or history.device.address, address=history.device.address, - rssi=history.device.rssi, + rssi=history.advertisement_data.rssi, manufacturer_data=history.advertisement_data.manufacturer_data, service_data=history.advertisement_data.service_data, service_uuids=history.advertisement_data.service_uuids, @@ -46,65 +32,10 @@ async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBlea connectable=False, time=now, ) - for address, history in bluez_dbus.history.items() + for address, history in adapters.history.items() } -async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: - """Return a list of bluetooth adapters.""" - if platform.system() == "Windows": - return { - WINDOWS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( - address=DEFAULT_ADDRESS, - sw_version=platform.release(), - passive_scan=False, - ) - } - if platform.system() == "Darwin": - return { - MACOS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( - address=DEFAULT_ADDRESS, - sw_version=platform.release(), - passive_scan=False, - ) - } - from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel - get_bluetooth_adapter_details, - ) - - adapters: dict[str, AdapterDetails] = {} - adapter_details = await get_bluetooth_adapter_details() - for adapter, details in adapter_details.items(): - adapter1 = details["org.bluez.Adapter1"] - adapters[adapter] = AdapterDetails( - address=adapter1["Address"], - sw_version=adapter1["Name"], # This is actually the BlueZ version - hw_version=adapter1.get("Modalias"), - passive_scan="org.bluez.AdvertisementMonitorManager1" in details, - ) - return adapters - - -@callback -def async_default_adapter() -> str: - """Return the default adapter for the platform.""" - return DEFAULT_ADAPTER_BY_PLATFORM.get( - platform.system(), UNIX_DEFAULT_BLUETOOTH_ADAPTER - ) - - -@callback -def adapter_human_name(adapter: str, address: str) -> str: - """Return a human readable name for the adapter.""" - return adapter if address == DEFAULT_ADDRESS else f"{adapter} ({address})" - - -@callback -def adapter_unique_name(adapter: str, address: str) -> str: - """Return a unique name for the adapter.""" - return adapter if address == DEFAULT_ADDRESS else address - - async def async_reset_adapter(adapter: str | None) -> bool | None: """Reset the adapter.""" if adapter and adapter.startswith("hci"): diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py new file mode 100644 index 00000000000..b1b06d43e31 --- /dev/null +++ b/homeassistant/components/bluetooth/wrappers.py @@ -0,0 +1,261 @@ +"""Bleak wrappers for bluetooth.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable +import contextlib +from dataclasses import dataclass +import logging +from typing import Any, Final + +from bleak import BleakClient, BleakError +from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementDataCallback, BaseBleakScanner +from bleak_retry_connector import NO_RSSI_VALUE, ble_device_description, clear_cache + +from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +from homeassistant.helpers.frame import report + +from . import models +from .models import HaBluetoothConnector + +FILTER_UUIDS: Final = "UUIDs" +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class _HaWrappedBleakBackend: + """Wrap bleak backend to make it usable by Home Assistant.""" + + device: BLEDevice + client: type[BaseBleakClient] + + +class HaBleakScannerWrapper(BaseBleakScanner): + """A wrapper that uses the single instance.""" + + def __init__( + self, + *args: Any, + detection_callback: AdvertisementDataCallback | None = None, + service_uuids: list[str] | None = None, + **kwargs: Any, + ) -> None: + """Initialize the BleakScanner.""" + self._detection_cancel: CALLBACK_TYPE | None = None + self._mapped_filters: dict[str, set[str]] = {} + self._advertisement_data_callback: AdvertisementDataCallback | None = None + remapped_kwargs = { + "detection_callback": detection_callback, + "service_uuids": service_uuids or [], + **kwargs, + } + self._map_filters(*args, **remapped_kwargs) + super().__init__( + detection_callback=detection_callback, service_uuids=service_uuids or [] + ) + + @classmethod + async def discover(cls, timeout: float = 5.0, **kwargs: Any) -> list[BLEDevice]: + """Discover devices.""" + assert models.MANAGER is not None + return list(models.MANAGER.async_discovered_devices(True)) + + async def stop(self, *args: Any, **kwargs: Any) -> None: + """Stop scanning for devices.""" + + async def start(self, *args: Any, **kwargs: Any) -> None: + """Start scanning for devices.""" + + def _map_filters(self, *args: Any, **kwargs: Any) -> bool: + """Map the filters.""" + mapped_filters = {} + if filters := kwargs.get("filters"): + if filter_uuids := filters.get(FILTER_UUIDS): + mapped_filters[FILTER_UUIDS] = set(filter_uuids) + else: + _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) + if service_uuids := kwargs.get("service_uuids"): + mapped_filters[FILTER_UUIDS] = set(service_uuids) + if mapped_filters == self._mapped_filters: + return False + self._mapped_filters = mapped_filters + return True + + def set_scanning_filter(self, *args: Any, **kwargs: Any) -> None: + """Set the filters to use.""" + if self._map_filters(*args, **kwargs): + self._setup_detection_callback() + + def _cancel_callback(self) -> None: + """Cancel callback.""" + if self._detection_cancel: + self._detection_cancel() + self._detection_cancel = None + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + assert models.MANAGER is not None + return list(models.MANAGER.async_discovered_devices(True)) + + def register_detection_callback( + self, callback: AdvertisementDataCallback | None + ) -> None: + """Register a callback that is called when a device is discovered or has a property changed. + + This method takes the callback and registers it with the long running + scanner. + """ + self._advertisement_data_callback = callback + self._setup_detection_callback() + + def _setup_detection_callback(self) -> None: + """Set up the detection callback.""" + if self._advertisement_data_callback is None: + return + self._cancel_callback() + super().register_detection_callback(self._advertisement_data_callback) + assert models.MANAGER is not None + assert self._callback is not None + self._detection_cancel = models.MANAGER.async_register_bleak_callback( + self._callback, self._mapped_filters + ) + + def __del__(self) -> None: + """Delete the BleakScanner.""" + if self._detection_cancel: + # Nothing to do if event loop is already closed + with contextlib.suppress(RuntimeError): + asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) + + +class HaBleakClientWrapper(BleakClient): + """Wrap the BleakClient to ensure it does not shutdown our scanner. + + If an address is passed into BleakClient instead of a BLEDevice, + bleak will quietly start a new scanner under the hood to resolve + the address. This can cause a conflict with our scanner. We need + to handle translating the address to the BLEDevice in this case + to avoid the whole stack from getting stuck in an in progress state + when an integration does this. + """ + + def __init__( # pylint: disable=super-init-not-called, keyword-arg-before-vararg + self, + address_or_ble_device: str | BLEDevice, + disconnected_callback: Callable[[BleakClient], None] | None = None, + *args: Any, + timeout: float = 10.0, + **kwargs: Any, + ) -> None: + """Initialize the BleakClient.""" + if isinstance(address_or_ble_device, BLEDevice): + self.__address = address_or_ble_device.address + else: + report( + "attempted to call BleakClient with an address instead of a BLEDevice", + exclude_integrations={"bluetooth"}, + error_if_core=False, + ) + self.__address = address_or_ble_device + self.__disconnected_callback = disconnected_callback + self.__timeout = timeout + self._backend: BaseBleakClient | None = None # type: ignore[assignment] + + @property + def is_connected(self) -> bool: + """Return True if the client is connected to a device.""" + return self._backend is not None and self._backend.is_connected + + async def clear_cache(self) -> bool: + """Clear the GATT cache.""" + if self._backend is not None and hasattr(self._backend, "clear_cache"): + return await self._backend.clear_cache() # type: ignore[no-any-return] + return await clear_cache(self.__address) + + def set_disconnected_callback( + self, + callback: Callable[[BleakClient], None] | None, + **kwargs: Any, + ) -> None: + """Set the disconnect callback.""" + self.__disconnected_callback = callback + if self._backend: + self._backend.set_disconnected_callback(callback, **kwargs) # type: ignore[arg-type] + + async def connect(self, **kwargs: Any) -> bool: + """Connect to the specified GATT server.""" + assert models.MANAGER is not None + wrapped_backend = self._async_get_best_available_backend_and_device() + self._backend = wrapped_backend.client( + wrapped_backend.device, + disconnected_callback=self.__disconnected_callback, + timeout=self.__timeout, + hass=models.MANAGER.hass, + ) + if debug_logging := _LOGGER.isEnabledFor(logging.DEBUG): + # Only lookup the description if we are going to log it + description = ble_device_description(wrapped_backend.device) + rssi = wrapped_backend.device.rssi + _LOGGER.debug("%s: Connecting (last rssi: %s)", description, rssi) + connected = await super().connect(**kwargs) + if debug_logging: + _LOGGER.debug("%s: Connected (last rssi: %s)", description, rssi) + return connected + + @hass_callback + def _async_get_backend_for_ble_device( + self, ble_device: BLEDevice + ) -> _HaWrappedBleakBackend | None: + """Get the backend for a BLEDevice.""" + details = ble_device.details + if not isinstance(details, dict) or "connector" not in details: + # If client is not defined in details + # its the client for this platform + cls = get_platform_client_backend_type() + return _HaWrappedBleakBackend(ble_device, cls) + + connector: HaBluetoothConnector = details["connector"] + # Make sure the backend can connect to the device + # as some backends have connection limits + if not connector.can_connect(): + return None + + return _HaWrappedBleakBackend(ble_device, connector.client) + + @hass_callback + def _async_get_best_available_backend_and_device( + self, + ) -> _HaWrappedBleakBackend: + """Get a best available backend and device for the given address. + + This method will return the backend with the best rssi + that has a free connection slot. + """ + assert models.MANAGER is not None + address = self.__address + device_advertisement_datas = models.MANAGER.async_get_discovered_devices_and_advertisement_data_by_address( + address, True + ) + for device_advertisement_data in sorted( + device_advertisement_datas, + key=lambda device_advertisement_data: device_advertisement_data[1].rssi + or NO_RSSI_VALUE, + reverse=True, + ): + if backend := self._async_get_backend_for_ble_device( + device_advertisement_data[0] + ): + return backend + + raise BleakError( + f"No backend with an available connection slot that can reach address {address} was found" + ) + + async def disconnect(self) -> bool: + """Disconnect from the device.""" + if self._backend is None: + return True + return await self._backend.disconnect() diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 3994b0732a8..4f05794e311 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -94,14 +94,9 @@ class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return BMWOptionsFlow(config_entry) -class BMWOptionsFlow(config_entries.OptionsFlow): +class BMWOptionsFlow(config_entries.OptionsFlowWithConfigEntry): """Handle a option flow for MyBMW.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize MyBMW option flow.""" - self.config_entry = config_entry - self.options = dict(config_entry.options) - async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index d26a63f8c0e..fc33335fe24 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -6,8 +6,7 @@ from typing import Any from bimmer_connected.vehicle import MyBMWVehicle -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/bmw_connected_drive/translations/sk.json b/homeassistant/components/bmw_connected_drive/translations/sk.json index 5ada995aa6e..bf40df63e99 100644 --- a/homeassistant/components/bmw_connected_drive/translations/sk.json +++ b/homeassistant/components/bmw_connected_drive/translations/sk.json @@ -1,7 +1,28 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Len na \u010d\u00edtanie (len senzory a notifik\u00e1cie, \u017eiadne vykon\u00e1vanie slu\u017eieb, \u017eiadny z\u00e1mok)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 0a3e9048451..a41e188ed9d 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -58,7 +58,7 @@ class BondCover(BondEntity, CoverEntity): ) -> None: """Create HA entity representing Bond cover.""" super().__init__(hub, device, bpup_subs) - supported_features = 0 + supported_features = CoverEntityFeature(0) if self._device.supports_set_position(): supported_features |= CoverEntityFeature.SET_POSITION if self._device.supports_open(): diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index d1121e4a3a8..bd4f01bce52 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -82,9 +82,9 @@ class BondFan(BondEntity, FanEntity): self._attr_preset_mode = PRESET_MODE_BREEZE if breeze[0] else None @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - features = 0 + features = FanEntityFeature(0) if self._device.supports_speed(): features |= FanEntityFeature.SET_SPEED if self._device.supports_direction(): diff --git a/homeassistant/components/bond/translations/sk.json b/homeassistant/components/bond/translations/sk.json index e237bd34b0a..1c17bead986 100644 --- a/homeassistant/components/bond/translations/sk.json +++ b/homeassistant/components/bond/translations/sk.json @@ -1,17 +1,26 @@ { "config": { - "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "old_firmware": "Nepodporovan\u00fd star\u00fd firmv\u00e9r na zariaden\u00ed Bond \u2013 pred pokra\u010dovan\u00edm aktualizujte", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { "access_token": "Pr\u00edstupov\u00fd token" - } + }, + "description": "Chcete nastavi\u0165 {name}?" }, "user": { "data": { - "access_token": "Pr\u00edstupov\u00fd token" + "access_token": "Pr\u00edstupov\u00fd token", + "host": "Hostite\u013e" } } } diff --git a/homeassistant/components/bosch_shc/translations/bg.json b/homeassistant/components/bosch_shc/translations/bg.json index a0b0548b51b..55de74f88a6 100644 --- a/homeassistant/components/bosch_shc/translations/bg.json +++ b/homeassistant/components/bosch_shc/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/bosch_shc/translations/sk.json b/homeassistant/components/bosch_shc/translations/sk.json index 71a7aea5018..aa59cecc5fb 100644 --- a/homeassistant/components/bosch_shc/translations/sk.json +++ b/homeassistant/components/bosch_shc/translations/sk.json @@ -1,10 +1,35 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Stla\u010dte tla\u010didlo na prednej strane ovl\u00e1da\u010da Bosch Smart Home Controller, k\u00fdm LED neza\u010dne blika\u0165.\n Ste pripraven\u00ed pokra\u010dova\u0165 v nastavovan\u00ed {model} @ {host} pomocou Home Assistant?" + }, + "credentials": { + "data": { + "password": "Heslo ovl\u00e1da\u010da Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "Integr\u00e1cia bosch_shc mus\u00ed znova overi\u0165 v\u00e1\u0161 \u00fa\u010det", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Nastavte svoj ovl\u00e1da\u010d Bosch Smart Home Controller tak, aby umo\u017e\u0148oval monitorovanie a ovl\u00e1danie pomocou Home Assistant.", + "title": "Parametre autentifik\u00e1cie SHC" + } } } } \ No newline at end of file diff --git a/homeassistant/components/brandt/__init__.py b/homeassistant/components/brandt/__init__.py new file mode 100644 index 00000000000..d2c9435c863 --- /dev/null +++ b/homeassistant/components/brandt/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Brandt Smart Control.""" diff --git a/homeassistant/components/brandt/manifest.json b/homeassistant/components/brandt/manifest.json new file mode 100644 index 00000000000..4fab6b2ec16 --- /dev/null +++ b/homeassistant/components/brandt/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "brandt", + "name": "Brandt Smart Control", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 45a2bad0036..183e13a19e8 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -245,22 +245,17 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}" -class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): +class BraviaTVOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): """Config flow options for Bravia TV.""" - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize Bravia TV options flow.""" - self.config_entry = config_entry - self.ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES) - self.source_list: list[str] = [] + data_schema: vol.Schema async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - coordinator: BraviaTVCoordinator = self.hass.data[DOMAIN][ - self.config_entry.entry_id - ] + coordinator: BraviaTVCoordinator + coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] try: await coordinator.async_update_sources() @@ -268,7 +263,13 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): return self.async_abort(reason="failed_update") sources = coordinator.source_map.values() - self.source_list = [item["title"] for item in sources] + source_list = [item["title"] for item in sources] + self.data_schema = vol.Schema( + { + vol.Optional(CONF_IGNORED_SOURCES): cv.multi_select(source_list), + } + ) + return await self.async_step_user() async def async_step_user( @@ -280,11 +281,7 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form( step_id="user", - data_schema=vol.Schema( - { - vol.Optional( - CONF_IGNORED_SOURCES, default=self.ignored_sources - ): cv.multi_select(self.source_list) - } + data_schema=self.add_suggested_values_to_schema( + self.data_schema, self.options ), ) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 1262e7bf7cc..3d57850a648 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -125,13 +125,18 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): """Connect and fetch data.""" try: if not self.connected: - if self.use_psk: - await self.client.connect(psk=self.pin) - else: - await self.client.connect( - pin=self.pin, clientid=self.client_id, nickname=self.nickname - ) - self.connected = True + try: + if self.use_psk: + await self.client.connect(psk=self.pin) + else: + await self.client.connect( + pin=self.pin, + clientid=self.client_id, + nickname=self.nickname, + ) + self.connected = True + except BraviaTVAuthError as err: + raise ConfigEntryAuthFailed from err power_status = await self.client.get_power_status() self.is_on = power_status == "active" @@ -151,8 +156,6 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err - except BraviaTVAuthError as err: - raise ConfigEntryAuthFailed from err except (BraviaTVConnectionError, BraviaTVConnectionTimeout, BraviaTVTurnedOff): self.is_on = False self.connected = False diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index 3a6908b0177..3eedf02caab 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -16,7 +16,7 @@ "authorize": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } }, "confirm": { @@ -25,7 +25,7 @@ "reauth_confirm": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } }, "user": { @@ -34,5 +34,10 @@ } } } + }, + "options": { + "abort": { + "failed_update": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0441\u043f\u0438\u0441\u044a\u043a\u0430 \u0441 \u0438\u0437\u0442\u043e\u0447\u043d\u0438\u0446\u0438.\n\n\u0423\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u044a\u0442 \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u0441\u0435 \u043e\u043f\u0438\u0442\u0430\u0442\u0435 \u0434\u0430 \u0433\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435." + } } } \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index e6f8ebc8e92..2b15a496af0 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "S'ha produ\u00eft un error en actualitzar la llista de fonts.\n\nAssegura't que el televisor est\u00e0 engegat abans de configurar-lo." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/cs.json b/homeassistant/components/braviatv/translations/cs.json index 59eed7f4187..583ad34efac 100644 --- a/homeassistant/components/braviatv/translations/cs.json +++ b/homeassistant/components/braviatv/translations/cs.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "not_bravia_device": "Za\u0159\u00edzen\u00ed nen\u00ed Bravia TV." }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa", "unsupported_model": "V\u00e1\u0161 model televize nen\u00ed podporov\u00e1n." }, diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index f62d496f2d3..8f376cdce9e 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -19,7 +19,7 @@ "pin": "PIN-Code", "use_psk": "PSK-Authentifizierung verwenden" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehe zu: Einstellungen - > Netzwerk - > Remote-Ger\u00e4teeinstellungen - > Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen - > Netzwerk - > Heimnetzwerk-Setup - > IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein.", + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Remote-Ger\u00e4teeinstellungen \u2192 Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Heimnetzwerk-Setup \u2192 IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein.", "title": "Autorisiere Sony Bravia TV" }, "confirm": { @@ -30,7 +30,7 @@ "pin": "PIN-Code", "use_psk": "PSK-Authentifizierung verwenden" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehe zu: Einstellungen - > Netzwerk - > Remote-Ger\u00e4teeinstellungen - > Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen - > Netzwerk - > Heimnetzwerk-Setup - > IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein." + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Remote-Ger\u00e4teeinstellungen \u2192 Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Heimnetzwerk-Setup \u2192 IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein." }, "user": { "data": { @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Beim Aktualisieren der Quellenliste ist ein Fehler aufgetreten. \n\nStelle sicher, dass dein Fernseher eingeschaltet ist, bevor du versuchst, ihn einzurichten." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index fc3ba88c57e..98ea6682eec 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03c0\u03b7\u03b3\u03ce\u03bd. \n\n \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c0\u03c1\u03b9\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index fbcd69a0bec..c467c5dafca 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Ocurri\u00f3 un error al actualizar la lista de fuentes. \n\nAseg\u00farate de que tu TV est\u00e9 encendida antes de intentar configurarla." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index 4e3ca6333d4..c650b6abd9f 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Allikate loendi v\u00e4rskendamisel ilmnes viga. \n\n Enne teleri seadistamist veendu, et see oleks sisse l\u00fclitatud." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index 40445ec8062..d71bbd0e8ac 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -29,7 +29,8 @@ "data": { "pin": "Code PIN", "use_psk": "Utiliser l'authentification PSK" - } + }, + "description": "Saisissez le code PIN affich\u00e9 sur le t\u00e9l\u00e9viseur Sony Bravia. \n\nSi le code PIN n'est pas affich\u00e9, vous devez supprimer Home Assistant du t\u00e9l\u00e9viseur. Pour cela, allez dans : Param\u00e8tres -> R\u00e9seau -> Param\u00e8tres du p\u00e9riph\u00e9rique distant ->Supprimer le p\u00e9riph\u00e9rique distant. \n\nVous pouvez utiliser PSK (Pre-Shared-Key) au lieu du code PIN. PSK est une cl\u00e9 secr\u00e8te d\u00e9finie par l'utilisateur utilis\u00e9e pour le contr\u00f4le d'acc\u00e8s. Cette m\u00e9thode d'authentification est recommand\u00e9e car elle est plus stable. Pour activer PSK sur votre t\u00e9l\u00e9viseur, allez dans : Param\u00e8tres -> R\u00e9seau -> Configuration du r\u00e9seau domestique -> Contr\u00f4le IP. Cochez ensuite la case \"Utiliser l'authentification PSK\" et entrez votre PSK au lieu du code PIN." }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index 853dd7da29e..6c357e6e2bb 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -19,7 +19,7 @@ "pin": "Kode PIN", "use_psk": "Gunakan autentikasi PSK" }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN.", + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN.", "title": "Otorisasi TV Sony Bravia" }, "confirm": { @@ -30,7 +30,7 @@ "pin": "Kode PIN", "use_psk": "Gunakan autentikasi PSK" }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN." + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN." }, "user": { "data": { @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Terjadi kesalahan saat memperbarui daftar sumber.\n\nPastikan TV Anda sudah dihidupkan sebelum menyiapkan." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 7bf9bb98b5a..e17c961fa45 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Si \u00e8 verificato un errore durante l'aggiornamento dell'elenco delle fonti.\n\nAssicurati che il televisore sia acceso prima di provare a configurarlo." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index dec2157d6a3..a568e364c12 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Det oppsto en feil under oppdatering av kildelisten. \n\n S\u00f8rg for at TV-en er sl\u00e5tt p\u00e5 f\u00f8r du pr\u00f8ver \u00e5 sette den opp." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index adc3a67e603..53847bf7b2c 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Wyst\u0105pi\u0142 b\u0142\u0105d podczas aktualizowania listy \u017ar\u00f3de\u0142.\n\nUpewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony, zanim spr\u00f3bujesz go skonfigurowa\u0107." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index 7c5af6e2694..e048568e351 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Ocorreu um erro ao atualizar a lista de fontes. \n\n Certifique-se de que sua TV esteja ligada antes de tentar configur\u00e1-la." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 8416d9e5ede..299ad538bc6 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -10,7 +10,7 @@ "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "step": { @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. \n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/sk.json b/homeassistant/components/braviatv/translations/sk.json new file mode 100644 index 00000000000..6447484aac8 --- /dev/null +++ b/homeassistant/components/braviatv/translations/sk.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_ip_control": "Ovl\u00e1danie IP je na va\u0161om telev\u00edzore vypnut\u00e9 alebo telev\u00edzor nie je podporovan\u00fd.", + "not_bravia_device": "Zariadenie nie je telev\u00edzor Bravia.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "unsupported_model": "V\u00e1\u0161 model TV nie je podporovan\u00fd." + }, + "step": { + "authorize": { + "data": { + "pin": "PIN k\u00f3d" + }, + "title": "Autorizujte telev\u00edzor Sony Bravia" + }, + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + }, + "reauth_confirm": { + "data": { + "pin": "PIN k\u00f3d", + "use_psk": "Pou\u017eite autentifik\u00e1ciu PSK" + } + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Zoznam ignorovan\u00fdch zdrojov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index e30142c947b..c66ba705db1 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "\u66f4\u65b0\u4f86\u6e90\u5217\u8868\u6642\u767c\u751f\u932f\u8aa4\u3002\n\n\u8acb\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u65bc\u8a2d\u5b9a\u524d\u958b\u555f\u3002" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 4ae0e39cf04..04a4d284161 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,7 +2,7 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.18.2"], + "requirements": ["broadlink==0.18.3"], "codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json index 65ee1f4db1d..85fd5db28e4 100644 --- a/homeassistant/components/broadlink/translations/ru.json +++ b/homeassistant/components/broadlink/translations/ru.json @@ -4,13 +4,13 @@ "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.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "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_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\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." }, "flow_title": "{name} ({model}, {host})", diff --git a/homeassistant/components/broadlink/translations/sk.json b/homeassistant/components/broadlink/translations/sk.json index 358fdc848ff..88c402f5a9a 100644 --- a/homeassistant/components/broadlink/translations/sk.json +++ b/homeassistant/components/broadlink/translations/sk.json @@ -1,13 +1,39 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "not_supported": "Zariadenie nie je podporovan\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({model} na {host})", "step": { "finish": { "data": { "name": "N\u00e1zov" } + }, + "reset": { + "title": "Odomknite zariadenie" + }, + "unlock": { + "data": { + "unlock": "\u00c1no, urobte to." + }, + "title": "Odomknite zariadenie (volite\u013en\u00e9)" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "title": "Pripojte sa k zariadeniu" } } } diff --git a/homeassistant/components/brother/translations/bg.json b/homeassistant/components/brother/translations/bg.json index 4a51b0abde1..06e4073839d 100644 --- a/homeassistant/components/brother/translations/bg.json +++ b/homeassistant/components/brother/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unsupported_model": "\u0422\u043e\u0437\u0438 \u043c\u043e\u0434\u0435\u043b \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/brother/translations/it.json b/homeassistant/components/brother/translations/it.json index 21fd1bf15ab..038815ec142 100644 --- a/homeassistant/components/brother/translations/it.json +++ b/homeassistant/components/brother/translations/it.json @@ -22,7 +22,7 @@ "type": "Tipo di stampante" }, "description": "Vuoi aggiungere la stampante {model} con il numero seriale `{serial_number}` a Home Assistant?", - "title": "Trovata stampante Brother" + "title": "Rilevata stampante Brother" } } } diff --git a/homeassistant/components/brother/translations/ru.json b/homeassistant/components/brother/translations/ru.json index a9f6158ccf8..1469e9962ed 100644 --- a/homeassistant/components/brother/translations/ru.json +++ b/homeassistant/components/brother/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", - "wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{model} {serial_number}", "step": { diff --git a/homeassistant/components/brother/translations/sk.json b/homeassistant/components/brother/translations/sk.json new file mode 100644 index 00000000000..f666e5524d6 --- /dev/null +++ b/homeassistant/components/brother/translations/sk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "unsupported_model": "Tento model tla\u010diarne nie je podporovan\u00fd." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "snmp_error": "Server SNMP je vypnut\u00fd alebo tla\u010diare\u0148 nie je podporovan\u00e1.", + "wrong_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa." + }, + "flow_title": "{model} {serial_number}", + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "type": "Typ tla\u010diarne" + } + }, + "zeroconf_confirm": { + "data": { + "type": "Typ tla\u010diarne" + }, + "description": "Chcete prida\u0165 tla\u010diare\u0148 {model} so s\u00e9riov\u00fdm \u010d\u00edslom `{serial_number}` do Home Assistant?", + "title": "Zisten\u00e1 tla\u010diare\u0148 Brother" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/bg.json b/homeassistant/components/brunt/translations/bg.json index 71737c4fb26..57d5f6be9bf 100644 --- a/homeassistant/components/brunt/translations/bg.json +++ b/homeassistant/components/brunt/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/brunt/translations/sk.json b/homeassistant/components/brunt/translations/sk.json index 3f00701d0ba..a654a071ab9 100644 --- a/homeassistant/components/brunt/translations/sk.json +++ b/homeassistant/components/brunt/translations/sk.json @@ -1,14 +1,25 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Znova zadajte heslo pre: {username}", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { + "password": "Heslo", "username": "U\u017e\u00edvate\u013esk\u00e9 meno" } } diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 6ee58989150..0ef3ed159a6 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -1,7 +1,7 @@ """The BSB-Lan integration.""" import dataclasses -from bsblan import BSBLAN, Device, Info, State +from bsblan import BSBLAN, Device, Info, State, StaticState from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -28,6 +28,7 @@ class HomeAssistantBSBLANData: client: BSBLAN device: Device info: Info + static: StaticState async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -54,11 +55,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = await bsblan.device() info = await bsblan.info() + static = await bsblan.static_values() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantBSBLANData( client=bsblan, coordinator=coordinator, device=device, info=info, + static=static, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index e9774055a85..acf9ee25c57 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from bsblan import BSBLAN, BSBLANError, Device, Info, State +from bsblan import BSBLAN, BSBLANError, Device, Info, State, StaticState from homeassistant.components.climate import ( ATTR_HVAC_MODE, @@ -15,7 +15,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -56,6 +56,7 @@ async def async_setup_entry( data.client, data.device, data.info, + data.static, entry, ) ], @@ -83,20 +84,21 @@ class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity): client: BSBLAN, device: Device, info: Info, + static: StaticState, entry: ConfigEntry, ) -> None: """Initialize BSBLAN climate device.""" - super().__init__(client, device, info, entry) + super().__init__(client, device, info, static, entry) CoordinatorEntity.__init__(self, coordinator) self._attr_unique_id = f"{format_mac(device.MAC)}-climate" - self._attr_min_temp = float(self.coordinator.data.min_temp.value) - self._attr_max_temp = float(self.coordinator.data.max_temp.value) - self._attr_temperature_unit = ( - TEMP_CELSIUS - if self.coordinator.data.current_temperature.unit == "°C" - else TEMP_FAHRENHEIT - ) + self._attr_min_temp = float(static.min_temp.value) + self._attr_max_temp = float(static.max_temp.value) + # check if self.coordinator.data.current_temperature.unit is "°C" or "°C" + if self.coordinator.data.current_temperature.unit in ("°C", "°C"): + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + else: + self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT @property def current_temperature(self) -> float | None: diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py new file mode 100644 index 00000000000..91d959ea0e2 --- /dev/null +++ b/homeassistant/components/bsblan/diagnostics.py @@ -0,0 +1,22 @@ +"""Diagnostics support for BSBLan.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import HomeAssistantBSBLANData +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data: HomeAssistantBSBLANData = hass.data[DOMAIN][entry.entry_id] + return { + "info": data.info.dict(), + "device": data.device.dict(), + "state": data.coordinator.data.dict(), + } diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py index 3e8a493d53b..c9b2a2ae9ae 100644 --- a/homeassistant/components/bsblan/entity.py +++ b/homeassistant/components/bsblan/entity.py @@ -1,7 +1,7 @@ """Base entity for the BSBLAN integration.""" from __future__ import annotations -from bsblan import BSBLAN, Device, Info +from bsblan import BSBLAN, Device, Info, StaticState from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -19,6 +19,7 @@ class BSBLANEntity(Entity): client: BSBLAN, device: Device, info: Info, + static: StaticState, entry: ConfigEntry, ) -> None: """Initialize an BSBLAN entity.""" diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 7c5422d3eff..810e78872f3 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -3,7 +3,7 @@ "name": "BSB-Lan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bsblan", - "requirements": ["python-bsblan==0.5.5"], + "requirements": ["python-bsblan==0.5.8"], "codeowners": ["@liudger"], "iot_class": "local_polling", "loggers": ["bsblan"] diff --git a/homeassistant/components/bsblan/translations/sk.json b/homeassistant/components/bsblan/translations/sk.json index 892b8b2cd91..40a6e13da9a 100644 --- a/homeassistant/components/bsblan/translations/sk.json +++ b/homeassistant/components/bsblan/translations/sk.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", "step": { "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index a048f9202b6..bbd77f271a4 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -22,9 +22,10 @@ from homeassistant.components.bluetooth.passive_update_processor import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { BTHomeBinarySensorDeviceClass.BATTERY: BinarySensorEntityDescription( @@ -147,7 +148,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a binary sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/bthome/device.py b/homeassistant/components/bthome/device.py index bd011752db1..eecd8161d6c 100644 --- a/homeassistant/components/bthome/device.py +++ b/homeassistant/components/bthome/device.py @@ -1,13 +1,11 @@ """Support for BTHome Bluetooth devices.""" from __future__ import annotations -from bthome_ble import DeviceKey, SensorDeviceInfo +from bthome_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo def device_key_to_bluetooth_entity_key( @@ -15,17 +13,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 3b4cbe2f4f4..bf447e6a485 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -11,9 +11,13 @@ { "connectable": false, "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" + }, + { + "connectable": false, + "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==1.2.2"], + "requirements": ["bthome-ble==2.3.1"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 9d68ce2d3b4..6493b291085 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -21,29 +21,34 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, + DEGREE, + ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, LIGHT_LUX, - MASS_KILOGRAMS, - MASS_POUNDS, PERCENTAGE, - POWER_WATT, - PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, + TIME_SECONDS, + UnitOfEnergy, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { (BTHomeSensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), (BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( @@ -61,7 +66,7 @@ SENSOR_DESCRIPTIONS = { (BTHomeSensorDeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, - native_unit_of_measurement=PRESSURE_MBAR, + native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, ), (BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( @@ -86,13 +91,13 @@ SENSOR_DESCRIPTIONS = { ): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.ENERGY}_{Units.ENERGY_KILO_WATT_HOUR}", device_class=SensorDeviceClass.ENERGY, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), (BTHomeSensorDeviceClass.POWER, Units.POWER_WATT): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.POWER}_{Units.POWER_WATT}", device_class=SensorDeviceClass.POWER, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), ( @@ -146,14 +151,14 @@ SENSOR_DESCRIPTIONS = { (BTHomeSensorDeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_KILOGRAMS}", device_class=SensorDeviceClass.WEIGHT, - native_unit_of_measurement=MASS_KILOGRAMS, + native_unit_of_measurement=UnitOfMass.KILOGRAMS, state_class=SensorStateClass.MEASUREMENT, ), # Used for mass sensor with lb unit (BTHomeSensorDeviceClass.MASS, Units.MASS_POUNDS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_POUNDS}", device_class=SensorDeviceClass.WEIGHT, - native_unit_of_measurement=MASS_POUNDS, + native_unit_of_measurement=UnitOfMass.POUNDS, state_class=SensorStateClass.MEASUREMENT, ), # Used for moisture sensor @@ -167,14 +172,67 @@ SENSOR_DESCRIPTIONS = { (BTHomeSensorDeviceClass.DEW_POINT, Units.TEMP_CELSIUS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), # Used for count sensor (BTHomeSensorDeviceClass.COUNT, None): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.COUNT}", - device_class=None, - native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for rotation sensor + (BTHomeSensorDeviceClass.ROTATION, Units.DEGREE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.ROTATION}_{Units.DEGREE}", + native_unit_of_measurement=DEGREE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for distance sensor in mm + ( + BTHomeSensorDeviceClass.DISTANCE, + Units.LENGTH_MILLIMETERS, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.DISTANCE}_{Units.LENGTH_MILLIMETERS}", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for distance sensor in m + (BTHomeSensorDeviceClass.DISTANCE, Units.LENGTH_METERS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.DISTANCE}_{Units.LENGTH_METERS}", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.METERS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for duration sensor + (BTHomeSensorDeviceClass.DURATION, Units.TIME_SECONDS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.DURATION}_{Units.TIME_SECONDS}", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_SECONDS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for current sensor + ( + BTHomeSensorDeviceClass.CURRENT, + Units.ELECTRIC_CURRENT_AMPERE, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.CURRENT}_{Units.ELECTRIC_CURRENT_AMPERE}", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for speed sensor + ( + BTHomeSensorDeviceClass.SPEED, + Units.SPEED_METERS_PER_SECOND, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.SPEED}_{Units.SPEED_METERS_PER_SECOND}", + device_class=SensorDeviceClass.SPEED, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for UV index sensor + (BTHomeSensorDeviceClass.UV_INDEX, None,): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.UV_INDEX}", state_class=SensorStateClass.MEASUREMENT, ), } @@ -186,7 +244,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/bthome/translations/bg.json b/homeassistant/components/bthome/translations/bg.json index 895fcac7c4f..22080c02972 100644 --- a/homeassistant/components/bthome/translations/bg.json +++ b/homeassistant/components/bthome/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bthome/translations/he.json b/homeassistant/components/bthome/translations/he.json index b90a366130a..0df85dd1fe5 100644 --- a/homeassistant/components/bthome/translations/he.json +++ b/homeassistant/components/bthome/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "flow_title": "{name}", diff --git a/homeassistant/components/bthome/translations/sk.json b/homeassistant/components/bthome/translations/sk.json new file mode 100644 index 00000000000..3767640fbf2 --- /dev/null +++ b/homeassistant/components/bthome/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "expected_32_characters": "O\u010dak\u00e1van\u00fd 32-znakov\u00fd hexadecim\u00e1lny bindkey." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/config_flow.py b/homeassistant/components/buienradar/config_flow.py index 445c6cacbc8..87810edda2e 100644 --- a/homeassistant/components/buienradar/config_flow.py +++ b/homeassistant/components/buienradar/config_flow.py @@ -1,7 +1,8 @@ """Config flow for buienradar integration.""" from __future__ import annotations -from typing import Any +import copy +from typing import Any, cast import voluptuous as vol @@ -10,7 +11,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import ( CONF_COUNTRY, @@ -23,6 +30,47 @@ from .const import ( SUPPORTED_COUNTRY_CODES, ) +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_COUNTRY, default=DEFAULT_COUNTRY): vol.In( + SUPPORTED_COUNTRY_CODES + ), + vol.Optional(CONF_DELTA, default=DEFAULT_DELTA): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + step=1, + mode=selector.NumberSelectorMode.BOX, + unit_of_measurement="seconds", + ), + ), + vol.Optional( + CONF_TIMEFRAME, default=DEFAULT_TIMEFRAME + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=5, + max=120, + step=5, + mode=selector.NumberSelectorMode.BOX, + unit_of_measurement="minutes", + ), + ), + } +) + + +async def _options_suggested_values(handler: SchemaCommonFlowHandler) -> dict[str, Any]: + parent_handler = cast(SchemaOptionsFlowHandler, handler.parent_handler) + suggested_values = copy.deepcopy(dict(parent_handler.config_entry.data)) + suggested_values.update(parent_handler.options) + return suggested_values + + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep( + OPTIONS_SCHEMA, suggested_values=_options_suggested_values + ), +} + class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for buienradar.""" @@ -33,9 +81,9 @@ class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: ConfigEntry, - ) -> BuienradarOptionFlowHandler: + ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" - return BuienradarOptionFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -66,49 +114,3 @@ class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=data_schema, errors={}, ) - - -class BuienradarOptionFlowHandler(config_entries.OptionsFlow): - """Handle options.""" - - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_COUNTRY, - default=self.config_entry.options.get( - CONF_COUNTRY, - self.config_entry.data.get(CONF_COUNTRY, DEFAULT_COUNTRY), - ), - ): vol.In(SUPPORTED_COUNTRY_CODES), - vol.Optional( - CONF_DELTA, - default=self.config_entry.options.get( - CONF_DELTA, - self.config_entry.data.get(CONF_DELTA, DEFAULT_DELTA), - ), - ): vol.All(vol.Coerce(int), vol.Range(min=0)), - vol.Optional( - CONF_TIMEFRAME, - default=self.config_entry.options.get( - CONF_TIMEFRAME, - self.config_entry.data.get( - CONF_TIMEFRAME, DEFAULT_TIMEFRAME - ), - ), - ): vol.All(vol.Coerce(int), vol.Range(min=5, max=120)), - } - ), - ) diff --git a/homeassistant/components/buienradar/translations/sk.json b/homeassistant/components/buienradar/translations/sk.json index d77712e768a..825db0b86a7 100644 --- a/homeassistant/components/buienradar/translations/sk.json +++ b/homeassistant/components/buienradar/translations/sk.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 6fdf5c166ee..4cee98c07b7 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -41,11 +41,11 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_METERS, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -120,11 +120,11 @@ async def async_setup_entry( class BrWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_visibility_unit = LENGTH_METERS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_visibility_unit = UnitOfLength.METERS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__(self, data, config, coordinates): """Initialize the platform with a data instance and station name.""" diff --git a/homeassistant/components/button/translations/sk.json b/homeassistant/components/button/translations/sk.json index 5dfc88234c6..96f4352529b 100644 --- a/homeassistant/components/button/translations/sk.json +++ b/homeassistant/components/button/translations/sk.json @@ -2,6 +2,10 @@ "device_automation": { "action_type": { "press": "Stla\u010dte tla\u010didlo {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} bol stla\u010den\u00fd" } - } + }, + "title": "Tla\u010didlo" } \ No newline at end of file diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index cfc09df667a..c89b36ce636 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -1,21 +1,27 @@ """Support for Google Calendar event device sensors.""" from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Callable, Iterable import dataclasses import datetime from http import HTTPStatus +from itertools import groupby import logging import re from typing import Any, cast, final from aiohttp import web +from dateutil.rrule import rrulestr +import voluptuous as vol -from homeassistant.components import frontend, http +from homeassistant.components import frontend, http, websocket_api +from homeassistant.components.websocket_api import ERR_NOT_FOUND, ERR_NOT_SUPPORTED +from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -27,12 +33,29 @@ from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt +from .const import ( + CONF_EVENT, + EVENT_DESCRIPTION, + EVENT_END, + EVENT_RECURRENCE_ID, + EVENT_RECURRENCE_RANGE, + EVENT_RRULE, + EVENT_START, + EVENT_SUMMARY, + EVENT_UID, + CalendarEntityFeature, +) + _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = datetime.timedelta(seconds=60) +# Don't support rrules more often than daily +VALID_FREQS = {"DAILY", "WEEKLY", "MONTHLY", "YEARLY"} + + # mypy: disallow-any-generics @@ -49,6 +72,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, "calendar", "calendar", "hass:calendar" ) + websocket_api.async_register_command(hass, handle_calendar_event_create) + websocket_api.async_register_command(hass, handle_calendar_event_delete) + await component.async_setup(config) return True @@ -88,6 +114,10 @@ class CalendarEvent: description: str | None = None location: str | None = None + uid: str | None = None + recurrence_id: str | None = None + rrule: str | None = None + @property def start_datetime_local(self) -> datetime.datetime: """Return event start time as a local datetime.""" @@ -183,6 +213,30 @@ def is_offset_reached( return start + offset_time <= dt.now(start.tzinfo) +def _validate_rrule(value: Any) -> str: + """Validate a recurrence rule string.""" + if value is None: + raise vol.Invalid("rrule value is None") + + if not isinstance(value, str): + raise vol.Invalid("rrule value expected a string") + + try: + rrulestr(value) + except ValueError as err: + raise vol.Invalid(f"Invalid rrule: {str(err)}") from err + + # Example format: FREQ=DAILY;UNTIL=... + rule_parts = dict(s.split("=", 1) for s in value.split(";")) + if not (freq := rule_parts.get("FREQ")): + raise vol.Invalid("rrule did not contain FREQ") + + if freq not in VALID_FREQS: + raise vol.Invalid(f"Invalid frequency for rule: {value}") + + return str(value) + + class CalendarEntity(Entity): """Base class for calendar event entities.""" @@ -230,6 +284,19 @@ class CalendarEntity(Entity): """Return calendar events within a datetime range.""" raise NotImplementedError() + async def async_create_event(self, **kwargs: Any) -> None: + """Add a new event to calendar.""" + raise NotImplementedError() + + async def async_delete_event( + self, + uid: str, + recurrence_id: str | None = None, + recurrence_range: str | None = None, + ) -> None: + """Delete an event on the calendar.""" + raise NotImplementedError() + class CalendarEventView(http.HomeAssistantView): """View to retrieve calendar content.""" @@ -297,3 +364,139 @@ class CalendarListView(http.HomeAssistantView): calendar_list.append({"name": state.name, "entity_id": entity.entity_id}) return self.json(sorted(calendar_list, key=lambda x: cast(str, x["name"]))) + + +def _has_same_type(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]: + """Verify that all values are of the same type.""" + + def validate(obj: dict[str, Any]) -> dict[str, Any]: + """Test that all keys in the dict have values of the same type.""" + uniq_values = groupby(type(obj[k]) for k in keys) + if len(list(uniq_values)) > 1: + raise vol.Invalid(f"Expected all values to be the same type: {keys}") + return obj + + return validate + + +def _has_consistent_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]: + """Verify that all datetime values have a consistent timezone.""" + + def validate(obj: dict[str, Any]) -> dict[str, Any]: + """Test that all keys that are datetime values have the same timezone.""" + values = [obj[k] for k in keys] + if all(isinstance(value, datetime.datetime) for value in values): + uniq_values = groupby(value.tzinfo for value in values) + if len(list(uniq_values)) > 1: + raise vol.Invalid( + f"Expected all values to have the same timezone: {values}" + ) + return obj + + return validate + + +def _is_sorted(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]: + """Verify that the specified values are sequential.""" + + def validate(obj: dict[str, Any]) -> dict[str, Any]: + """Test that all keys in the dict are in order.""" + values = [obj[k] for k in keys] + if values != sorted(values): + raise vol.Invalid(f"Values were not in order: {values}") + return obj + + return validate + + +@websocket_api.websocket_command( + { + vol.Required("type"): "calendar/event/create", + vol.Required("entity_id"): cv.entity_id, + CONF_EVENT: vol.Schema( + vol.All( + { + vol.Required(EVENT_START): vol.Any(cv.date, cv.datetime), + vol.Required(EVENT_END): vol.Any(cv.date, cv.datetime), + vol.Required(EVENT_SUMMARY): cv.string, + vol.Optional(EVENT_DESCRIPTION): cv.string, + vol.Optional(EVENT_RRULE): _validate_rrule, + }, + _has_same_type(EVENT_START, EVENT_END), + _has_consistent_timezone(EVENT_START, EVENT_END), + _is_sorted(EVENT_START, EVENT_END), + ) + ), + } +) +@websocket_api.async_response +async def handle_calendar_event_create( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle creation of a calendar event.""" + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] + if not (entity := component.get_entity(msg["entity_id"])): + connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found") + return + + if ( + not entity.supported_features + or not entity.supported_features & CalendarEntityFeature.CREATE_EVENT + ): + connection.send_message( + websocket_api.error_message( + msg["id"], ERR_NOT_SUPPORTED, "Calendar does not support event creation" + ) + ) + return + + try: + await entity.async_create_event(**msg[CONF_EVENT]) + except HomeAssistantError as ex: + connection.send_error(msg["id"], "failed", str(ex)) + else: + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "calendar/event/delete", + vol.Required("entity_id"): cv.entity_id, + vol.Required(EVENT_UID): cv.string, + vol.Optional(EVENT_RECURRENCE_ID): cv.string, + vol.Optional(EVENT_RECURRENCE_RANGE): cv.string, + } +) +@websocket_api.async_response +async def handle_calendar_event_delete( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle delete of a calendar event.""" + + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] + if not (entity := component.get_entity(msg["entity_id"])): + connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found") + return + + if ( + not entity.supported_features + or not entity.supported_features & CalendarEntityFeature.DELETE_EVENT + ): + connection.send_message( + websocket_api.error_message( + msg["id"], ERR_NOT_SUPPORTED, "Calendar does not support event deletion" + ) + ) + return + + try: + await entity.async_delete_event( + msg[EVENT_UID], + recurrence_id=msg.get(EVENT_RECURRENCE_ID), + recurrence_range=msg.get(EVENT_RECURRENCE_RANGE), + ) + except (HomeAssistantError, ValueError) as ex: + _LOGGER.error("Error handling Calendar Event call: %s", ex) + connection.send_error(msg["id"], "failed", str(ex)) + else: + connection.send_result(msg["id"]) diff --git a/homeassistant/components/calendar/const.py b/homeassistant/components/calendar/const.py new file mode 100644 index 00000000000..adee190200a --- /dev/null +++ b/homeassistant/components/calendar/const.py @@ -0,0 +1,24 @@ +"""Constants for calendar components.""" + +from enum import IntEnum + +CONF_EVENT = "event" + + +class CalendarEntityFeature(IntEnum): + """Supported features of the calendar entity.""" + + CREATE_EVENT = 1 + DELETE_EVENT = 2 + + +# rfc5545 fields +EVENT_UID = "uid" +EVENT_START = "dtstart" +EVENT_END = "dtend" +EVENT_SUMMARY = "summary" +EVENT_DESCRIPTION = "description" +EVENT_LOCATION = "location" +EVENT_RECURRENCE_ID = "recurrence_id" +EVENT_RECURRENCE_RANGE = "recurrence_range" +EVENT_RRULE = "rrule" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d860776a797..51e874ef3b7 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -5,9 +5,9 @@ import asyncio import collections from collections.abc import Awaitable, Callable, Iterable from contextlib import suppress -from dataclasses import dataclass +from dataclasses import asdict, dataclass from datetime import datetime, timedelta -from enum import IntEnum +from enum import IntFlag from functools import partial import logging import os @@ -30,6 +30,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.stream import ( FORMAT_CONTENT_TYPE, OUTPUT_FORMATS, + Orientation, Stream, create_stream, ) @@ -73,7 +74,7 @@ from .const import ( # noqa: F401 StreamType, ) from .img_util import scale_jpeg_camera_image -from .prefs import CameraPreferences +from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 _LOGGER = logging.getLogger(__name__) @@ -94,7 +95,7 @@ STATE_STREAMING: Final = "streaming" STATE_IDLE: Final = "idle" -class CameraEntityFeature(IntEnum): +class CameraEntityFeature(IntFlag): """Supported features of the camera entity.""" ON_OFF = 1 @@ -345,7 +346,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) prefs = CameraPreferences(hass) - await prefs.async_initialize() hass.data[DATA_CAMERA_PREFS] = prefs hass.http.register_view(CameraImageView(component)) @@ -359,14 +359,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) async def preload_stream(_event: Event) -> None: - for camera in component.entities: - camera_prefs = prefs.get(camera.entity_id) - if not camera_prefs.preload_stream: + for camera in list(component.entities): + stream_prefs = await prefs.get_dynamic_stream_settings(camera.entity_id) + if not stream_prefs.preload_stream: continue stream = await camera.async_create_stream() if not stream: continue - stream.keepalive = True stream.add_provider("hls") await stream.start() @@ -430,7 +429,7 @@ class Camera(Entity): _attr_motion_detection_enabled: bool = False _attr_should_poll: bool = False # No need to poll cameras _attr_state: None = None # State is determined by is_on - _attr_supported_features: int = 0 + _attr_supported_features: CameraEntityFeature = CameraEntityFeature(0) def __init__(self) -> None: """Initialize a camera.""" @@ -451,7 +450,7 @@ class Camera(Entity): return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) @property - def supported_features(self) -> int: + def supported_features(self) -> CameraEntityFeature: """Flag supported features.""" return self._attr_supported_features @@ -523,6 +522,9 @@ class Camera(Entity): self.hass, source, options=self.stream_options, + dynamic_stream_settings=await self.hass.data[ + DATA_CAMERA_PREFS + ].get_dynamic_stream_settings(self.entity_id), stream_label=self.entity_id, ) self.stream.set_update_callback(self.async_write_ha_state) @@ -859,8 +861,9 @@ async def websocket_get_prefs( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle request for account info.""" - prefs = hass.data[DATA_CAMERA_PREFS].get(msg["entity_id"]) - connection.send_result(msg["id"], prefs.as_dict()) + prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS] + stream_prefs = await prefs.get_dynamic_stream_settings(msg["entity_id"]) + connection.send_result(msg["id"], asdict(stream_prefs)) @websocket_api.websocket_command( @@ -868,7 +871,7 @@ async def websocket_get_prefs( vol.Required("type"): "camera/update_prefs", vol.Required("entity_id"): cv.entity_id, vol.Optional(PREF_PRELOAD_STREAM): bool, - vol.Optional(PREF_ORIENTATION): vol.All(int, vol.Range(min=1, max=8)), + vol.Optional(PREF_ORIENTATION): vol.Coerce(Orientation), } ) @websocket_api.async_response @@ -876,7 +879,7 @@ async def websocket_update_prefs( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle request for account info.""" - prefs = hass.data[DATA_CAMERA_PREFS] + prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS] changes = dict(msg) changes.pop("id") @@ -954,11 +957,6 @@ async def _async_stream_endpoint_url( f"{camera.entity_id} does not support play stream service" ) - # Update keepalive setting which manages idle shutdown - camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) - stream.keepalive = camera_prefs.preload_stream - stream.orientation = camera_prefs.orientation - stream.add_provider(fmt) await stream.start() return stream.endpoint_url(fmt) diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 1107da2ba38..0a8785457e8 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,8 +1,10 @@ """Preference management for camera component.""" from __future__ import annotations +from dataclasses import asdict, dataclass from typing import Final, Union, cast +from homeassistant.components.stream import Orientation from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -15,26 +17,12 @@ STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 -class CameraEntityPreferences: - """Handle preferences for camera entity.""" +@dataclass +class DynamicStreamSettings: + """Stream settings which are managed and updated by the camera entity.""" - def __init__(self, prefs: dict[str, bool | int]) -> None: - """Initialize prefs.""" - self._prefs = prefs - - def as_dict(self) -> dict[str, bool | int]: - """Return dictionary version.""" - return self._prefs - - @property - def preload_stream(self) -> bool: - """Return if stream is loaded on hass start.""" - return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False)) - - @property - def orientation(self) -> int: - """Return the current stream orientation settings.""" - return self._prefs.get(PREF_ORIENTATION, 1) + preload_stream: bool = False + orientation: Orientation = Orientation.NO_TRANSFORM class CameraPreferences: @@ -45,39 +33,38 @@ class CameraPreferences: self._hass = hass # The orientation prefs are stored in in the entity registry options # The preload_stream prefs are stored in this Store - self._store = Store[dict[str, dict[str, Union[bool, int]]]]( + self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]]( hass, STORAGE_VERSION, STORAGE_KEY ) - # Local copy of the preload_stream prefs - self._prefs: dict[str, dict[str, bool | int]] | None = None - - async def async_initialize(self) -> None: - """Finish initializing the preferences.""" - if (prefs := await self._store.async_load()) is None: - prefs = {} - - self._prefs = prefs + self._dynamic_stream_settings_by_entity_id: dict[ + str, DynamicStreamSettings + ] = {} async def async_update( self, entity_id: str, *, preload_stream: bool | UndefinedType = UNDEFINED, - orientation: int | UndefinedType = UNDEFINED, - stream_options: dict[str, str] | UndefinedType = UNDEFINED, - ) -> dict[str, bool | int]: + orientation: Orientation | UndefinedType = UNDEFINED, + ) -> dict[str, bool | Orientation]: """Update camera preferences. + Also update the DynamicStreamSettings if they exist. + preload_stream is stored in a Store + orientation is stored in the Entity Registry + Returns a dict with the preferences on success. Raises HomeAssistantError on failure. """ + dynamic_stream_settings = self._dynamic_stream_settings_by_entity_id.get( + entity_id + ) if preload_stream is not UNDEFINED: - # Prefs already initialized. - assert self._prefs is not None - if not self._prefs.get(entity_id): - self._prefs[entity_id] = {} - self._prefs[entity_id][PREF_PRELOAD_STREAM] = preload_stream - await self._store.async_save(self._prefs) + if dynamic_stream_settings: + dynamic_stream_settings.preload_stream = preload_stream + preload_prefs = await self._store.async_load() or {} + preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream} + await self._store.async_save(preload_prefs) if orientation is not UNDEFINED: if (registry := er.async_get(self._hass)).async_get(entity_id): @@ -88,12 +75,26 @@ class CameraPreferences: raise HomeAssistantError( "Orientation is only supported on entities set up through config flows" ) - return self.get(entity_id).as_dict() + if dynamic_stream_settings: + dynamic_stream_settings.orientation = orientation + return asdict(await self.get_dynamic_stream_settings(entity_id)) - def get(self, entity_id: str) -> CameraEntityPreferences: - """Get preferences for an entity.""" - # Prefs are already initialized. - assert self._prefs is not None + async def get_dynamic_stream_settings( + self, entity_id: str + ) -> DynamicStreamSettings: + """Get the DynamicStreamSettings for the entity.""" + if settings := self._dynamic_stream_settings_by_entity_id.get(entity_id): + return settings + # Get preload stream setting from prefs + # Get orientation setting from entity registry reg_entry = er.async_get(self._hass).async_get(entity_id) er_prefs = reg_entry.options.get(DOMAIN, {}) if reg_entry else {} - return CameraEntityPreferences(self._prefs.get(entity_id, {}) | er_prefs) + preload_prefs = await self._store.async_load() or {} + settings = DynamicStreamSettings( + preload_stream=cast( + bool, preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False) + ), + orientation=er_prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM), + ) + self._dynamic_stream_settings_by_entity_id[entity_id] = settings + return settings diff --git a/homeassistant/components/camera/translations/de.json b/homeassistant/components/camera/translations/de.json index d6f409f1a0e..2dd79596cc3 100644 --- a/homeassistant/components/camera/translations/de.json +++ b/homeassistant/components/camera/translations/de.json @@ -1,7 +1,7 @@ { "state": { "_": { - "idle": "Unt\u00e4tig", + "idle": "Inaktiv", "recording": "Aufnehmen", "streaming": "\u00dcbertr\u00e4gt" } diff --git a/homeassistant/components/canary/translations/sk.json b/homeassistant/components/canary/translations/sk.json new file mode 100644 index 00000000000..4facc537244 --- /dev/null +++ b/homeassistant/components/canary/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index d7cddaaa293..c5dc56a7da0 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==12.1.4"], + "requirements": ["pychromecast==13.0.1"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index edd8e0331d9..786a530e36c 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -899,7 +899,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return self._chromecast.app_display_name if self._chromecast else None @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" support = ( MediaPlayerEntityFeature.PLAY_MEDIA diff --git a/homeassistant/components/cast/translations/sk.json b/homeassistant/components/cast/translations/sk.json index e227301685b..9fe44a03383 100644 --- a/homeassistant/components/cast/translations/sk.json +++ b/homeassistant/components/cast/translations/sk.json @@ -1,9 +1,32 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "invalid_known_hosts": "Zn\u00e1mi hostitelia musia by\u0165 v zozname hostite\u013eov oddelen\u00fd \u010diarkami." + }, "step": { + "config": { + "data": { + "known_hosts": "Zn\u00e1mi hostitelia" + } + }, "confirm": { "description": "Chcete za\u010da\u0165 nastavova\u0165?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Zn\u00e1mi hostitelia musia by\u0165 v zozname hostite\u013eov oddelen\u00fd \u010diarkami." + }, + "step": { + "basic_options": { + "data": { + "known_hosts": "Zn\u00e1mi hostitelia" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 1bcc2ec4f72..2d2b2c68a45 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -9,7 +9,7 @@ "step": { "config": { "data": { - "known_hosts": "K\u00e4nad v\u00e4rdar" + "known_hosts": "K\u00e4nda v\u00e4rdar" }, "description": "K\u00e4nda v\u00e4rdar - En kommaseparerad lista \u00f6ver v\u00e4rdnamn eller IP-adresser f\u00f6r cast-enheter, anv\u00e4nd om mDNS-uppt\u00e4ckt inte fungerar.", "title": "Google Cast-konfiguration" @@ -34,7 +34,7 @@ }, "basic_options": { "data": { - "known_hosts": "K\u00e4nad v\u00e4rdar" + "known_hosts": "K\u00e4nda v\u00e4rdar" }, "description": "K\u00e4nda v\u00e4rdar - En kommaseparerad lista \u00f6ver v\u00e4rdnamn eller IP-adresser f\u00f6r cast-enheter, anv\u00e4nd om mDNS-uppt\u00e4ckt inte fungerar.", "title": "Google Cast-konfiguration" diff --git a/homeassistant/components/cert_expiry/translations/sk.json b/homeassistant/components/cert_expiry/translations/sk.json index 892b8b2cd91..7147710b357 100644 --- a/homeassistant/components/cert_expiry/translations/sk.json +++ b/homeassistant/components/cert_expiry/translations/sk.json @@ -1,11 +1,24 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "import_failed": "Import z konfigur\u00e1cie zlyhal" + }, + "error": { + "connection_refused": "Pri prip\u00e1jan\u00ed k hostite\u013eovi bolo pripojenie odmietnut\u00e9", + "connection_timeout": "\u010casov\u00fd limit pri pripojen\u00ed k tomuto hostite\u013eovi", + "resolve_failed": "Tento hostite\u013e sa ned\u00e1 vyrie\u0161i\u0165" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", + "name": "N\u00e1zov certifik\u00e1tu", "port": "Port" - } + }, + "title": "Definujte certifik\u00e1t na testovanie" } } - } + }, + "title": "Platnos\u0165 certifik\u00e1tu" } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index 270d72bd58c..438007171f0 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -3,9 +3,9 @@ "step": { "init": { "data": { - "timestep": "Min. entre pron\u00f3sticos de NowCast" + "timestep": "Min. entre previsiones de NowCast" }, - "description": "Si eliges habilitar la entidad de pron\u00f3stico `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", + "description": "Si eliges habilitar la entidad de previsi\u00f3n `nowcast`, puedes configurar la cantidad de minutos entre cada previsi\u00f3n. La cantidad de previsiones proporcionados depende de la cantidad de minutos elegidos entre las mismas.", "title": "Actualizar opciones de ClimaCell" } } diff --git a/homeassistant/components/climacell/translations/sensor.sk.json b/homeassistant/components/climacell/translations/sensor.sk.json index 843169b2f3b..f94574da180 100644 --- a/homeassistant/components/climacell/translations/sensor.sk.json +++ b/homeassistant/components/climacell/translations/sensor.sk.json @@ -2,6 +2,18 @@ "state": { "climacell__health_concern": { "unhealthy": "Nezdrav\u00e9" + }, + "climacell__pollen_index": { + "low": "N\u00edzke", + "medium": "Stredn\u00e9", + "very_high": "Ve\u013emi vysok\u00e9", + "very_low": "Ve\u013emi n\u00edzke" + }, + "climacell__precipitation_type": { + "freezing_rain": "Mrzn\u00faci d\u00e1\u017e\u010f", + "ice_pellets": "\u013dadovec", + "rain": "D\u00e1\u017e\u010f", + "snow": "Sneh" } } } \ No newline at end of file diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 67348d38625..68338819421 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -225,7 +225,7 @@ class ClimateEntity(Entity): _attr_precision: float _attr_preset_mode: str | None _attr_preset_modes: list[str] | None - _attr_supported_features: int + _attr_supported_features: ClimateEntityFeature = ClimateEntityFeature(0) _attr_swing_mode: str | None _attr_swing_modes: list[str] | None _attr_target_humidity: int | None = None @@ -531,7 +531,7 @@ class ClimateEntity(Entity): async def async_turn_on(self) -> None: """Turn the entity on.""" if hasattr(self, "turn_on"): - await self.hass.async_add_executor_job(self.turn_on) # type: ignore[attr-defined] + await self.hass.async_add_executor_job(self.turn_on) return # Fake turn on @@ -544,7 +544,7 @@ class ClimateEntity(Entity): async def async_turn_off(self) -> None: """Turn the entity off.""" if hasattr(self, "turn_off"): - await self.hass.async_add_executor_job(self.turn_off) # type: ignore[attr-defined] + await self.hass.async_add_executor_job(self.turn_off) return # Fake turn off @@ -552,7 +552,7 @@ class ClimateEntity(Entity): await self.async_set_hvac_mode(HVACMode.OFF) @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" return self._attr_supported_features diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 202da1af597..9ee561b9c1b 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -1,6 +1,6 @@ """Provides the constants needed for component.""" -from enum import IntEnum +from enum import IntFlag from homeassistant.backports.enum import StrEnum @@ -146,7 +146,7 @@ SERVICE_SET_SWING_MODE = "set_swing_mode" SERVICE_SET_TEMPERATURE = "set_temperature" -class ClimateEntityFeature(IntEnum): +class ClimateEntityFeature(IntFlag): """Supported features of the climate entity.""" TARGET_TEMPERATURE = 1 diff --git a/homeassistant/components/climate/translations/et.json b/homeassistant/components/climate/translations/et.json index c57a4d72991..2f947675510 100644 --- a/homeassistant/components/climate/translations/et.json +++ b/homeassistant/components/climate/translations/et.json @@ -20,8 +20,8 @@ "cool": "Jahuta", "dry": "Kuivata", "fan_only": "Ventileeri", - "heat": "Soojenda", - "heat_cool": "K\u00fcta/jahuta", + "heat": "K\u00fcta", + "heat_cool": "K\u00fcta", "off": "V\u00e4ljas" } }, diff --git a/homeassistant/components/climate/translations/sk.json b/homeassistant/components/climate/translations/sk.json index 15536f9b879..0b964a90799 100644 --- a/homeassistant/components/climate/translations/sk.json +++ b/homeassistant/components/climate/translations/sk.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "current_humidity_changed": "{entity_name} sa zmenila nameran\u00e1 vlhkos\u0165", + "current_temperature_changed": "{entity_name} sa zmenila nameran\u00e1 teplota" + } + }, "state": { "_": { "auto": "Automatika", diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 6a948c0ad15..c5918dcf28f 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable +from datetime import timedelta from enum import Enum from hass_nabucasa import Cloud @@ -26,6 +27,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -34,27 +36,30 @@ from homeassistant.util.aiohttp import MockRequest from . import account_link, http_api from .client import CloudClient from .const import ( - CONF_ACCOUNT_LINK_URL, - CONF_ACME_DIRECTORY_SERVER, + CONF_ACCOUNT_LINK_SERVER, + CONF_ACCOUNTS_SERVER, + CONF_ACME_SERVER, CONF_ALEXA, - CONF_ALEXA_ACCESS_TOKEN_URL, + CONF_ALEXA_SERVER, CONF_ALIASES, - CONF_CLOUDHOOK_CREATE_URL, + CONF_CLOUDHOOK_SERVER, CONF_COGNITO_CLIENT_ID, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_GOOGLE_ACTIONS, - CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, - CONF_RELAYER, - CONF_REMOTE_API_URL, - CONF_SUBSCRIPTION_INFO_URL, + CONF_RELAYER_SERVER, + CONF_REMOTE_SNI_SERVER, + CONF_REMOTESTATE_SERVER, + CONF_THINGTALK_SERVER, CONF_USER_POOL_ID, - CONF_VOICE_API_URL, + CONF_VOICE_SERVER, DOMAIN, MODE_DEV, MODE_PROD, ) from .prefs import CloudPreferences +from .repairs import async_manage_legacy_subscription_issue +from .subscription import async_subscription_info DEFAULT_MODE = MODE_PROD @@ -103,17 +108,18 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_COGNITO_CLIENT_ID): str, vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_REGION): str, - vol.Optional(CONF_RELAYER): str, - vol.Optional(CONF_SUBSCRIPTION_INFO_URL): vol.Url(), - vol.Optional(CONF_CLOUDHOOK_CREATE_URL): vol.Url(), - vol.Optional(CONF_REMOTE_API_URL): vol.Url(), - vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(), vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, - vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), - vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), - vol.Optional(CONF_ACCOUNT_LINK_URL): vol.Url(), - vol.Optional(CONF_VOICE_API_URL): vol.Url(), + vol.Optional(CONF_ACCOUNT_LINK_SERVER): str, + vol.Optional(CONF_ACCOUNTS_SERVER): str, + vol.Optional(CONF_ACME_SERVER): str, + vol.Optional(CONF_ALEXA_SERVER): str, + vol.Optional(CONF_CLOUDHOOK_SERVER): str, + vol.Optional(CONF_RELAYER_SERVER): str, + vol.Optional(CONF_REMOTE_SNI_SERVER): str, + vol.Optional(CONF_REMOTESTATE_SERVER): str, + vol.Optional(CONF_THINGTALK_SERVER): str, + vol.Optional(CONF_VOICE_SERVER): str, } ) }, @@ -258,6 +264,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: loaded = False + async def async_startup_repairs(_=None) -> None: + """Create repair issues after startup.""" + if not cloud.is_logged_in: + return + + if subscription_info := await async_subscription_info(cloud): + async_manage_legacy_subscription_issue(hass, subscription_info) + async def _on_connect(): """Discover RemoteUI binary sensor.""" nonlocal loaded @@ -294,6 +308,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: account_link.async_setup(hass) + async_call_later( + hass=hass, + delay=timedelta(hours=1), + action=async_startup_repairs, + ) + return True diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index ea0240acccf..9fb4ffc7047 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -47,16 +47,18 @@ CONF_COGNITO_CLIENT_ID = "cognito_client_id" CONF_ENTITY_CONFIG = "entity_config" CONF_FILTER = "filter" CONF_GOOGLE_ACTIONS = "google_actions" -CONF_RELAYER = "relayer" CONF_USER_POOL_ID = "user_pool_id" -CONF_SUBSCRIPTION_INFO_URL = "subscription_info_url" -CONF_CLOUDHOOK_CREATE_URL = "cloudhook_create_url" -CONF_REMOTE_API_URL = "remote_api_url" -CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" -CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" -CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" -CONF_ACCOUNT_LINK_URL = "account_link_url" -CONF_VOICE_API_URL = "voice_api_url" + +CONF_ACCOUNT_LINK_SERVER = "account_link_server" +CONF_ACCOUNTS_SERVER = "accounts_server" +CONF_ACME_SERVER = "acme_server" +CONF_ALEXA_SERVER = "alexa_server" +CONF_CLOUDHOOK_SERVER = "cloudhook_server" +CONF_RELAYER_SERVER = "relayer_server" +CONF_REMOTE_SNI_SERVER = "remote_sni_server" +CONF_REMOTESTATE_SERVER = "remotestate_server" +CONF_THINGTALK_SERVER = "thingtalk_server" +CONF_VOICE_SERVER = "voice_server" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 01b6cd17508..76d8bea1664 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -9,7 +9,7 @@ from typing import Any import aiohttp import async_timeout import attr -from hass_nabucasa import Cloud, auth, cloud_api, thingtalk +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 @@ -38,6 +38,8 @@ from .const import ( PREF_TTS_DEFAULT_VOICE, REQUEST_TIMEOUT, ) +from .repairs import async_manage_legacy_subscription_issue +from .subscription import async_subscription_info _LOGGER = logging.getLogger(__name__) @@ -328,15 +330,14 @@ async def websocket_subscription( ) -> None: """Handle request for account info.""" cloud = hass.data[DOMAIN] - try: - async with async_timeout.timeout(REQUEST_TIMEOUT): - data = await cloud_api.async_subscription_info(cloud) - except aiohttp.ClientError: + if (data := await async_subscription_info(cloud)) is None: connection.send_error( msg["id"], "request_failed", "Failed to request subscription" ) - else: - connection.send_result(msg["id"], data) + return + + connection.send_result(msg["id"], data) + async_manage_legacy_subscription_issue(hass, data) @_require_cloud_login diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 97f581d3bf0..70049e2a426 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.56.0"], + "requirements": ["hass-nabucasa==0.61.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py new file mode 100644 index 00000000000..0d217521c21 --- /dev/null +++ b/homeassistant/components/cloud/repairs.py @@ -0,0 +1,121 @@ +"""Repairs implementation for the cloud integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from hass_nabucasa import Cloud +import voluptuous as vol + +from homeassistant.components.repairs import RepairsFlow, repairs_flow_manager +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import issue_registry as ir + +from .const import DOMAIN +from .subscription import async_subscription_info + +BACKOFF_TIME = 5 +MAX_RETRIES = 60 # This allows for 10 minutes of retries + + +@callback +def async_manage_legacy_subscription_issue( + hass: HomeAssistant, + subscription_info: dict[str, Any], +) -> None: + """ + Manage the legacy subscription issue. + + If the provider is "legacy" create an issue, + in all other cases remove the issue. + """ + if subscription_info.get("provider") == "legacy": + ir.async_create_issue( + hass=hass, + domain=DOMAIN, + issue_id="legacy_subscription", + is_fixable=True, + severity=ir.IssueSeverity.WARNING, + translation_key="legacy_subscription", + ) + return + ir.async_delete_issue(hass=hass, domain=DOMAIN, issue_id="legacy_subscription") + + +class LegacySubscriptionRepairFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + wait_task: asyncio.Task | None = None + _data: dict[str, Any] | None = None + + async def async_step_init(self, _: None = None) -> FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm_change_plan() + + async def async_step_confirm_change_plan( + self, + user_input: dict[str, str] | None = None, + ) -> FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return await self.async_step_change_plan() + + return self.async_show_form( + step_id="confirm_change_plan", data_schema=vol.Schema({}) + ) + + async def async_step_change_plan(self, _: None = None) -> FlowResult: + """Wait for the user to authorize the app installation.""" + + async def _async_wait_for_plan_change() -> None: + flow_manager = repairs_flow_manager(self.hass) + # We can not get here without a flow manager + assert flow_manager is not None + + cloud: Cloud = self.hass.data[DOMAIN] + + retries = 0 + while retries < MAX_RETRIES: + self._data = await async_subscription_info(cloud) + if self._data is not None and self._data["provider"] != "legacy": + break + + retries += 1 + await asyncio.sleep(BACKOFF_TIME) + + self.hass.async_create_task( + flow_manager.async_configure(flow_id=self.flow_id) + ) + + if not self.wait_task: + self.wait_task = self.hass.async_create_task(_async_wait_for_plan_change()) + return self.async_external_step( + step_id="change_plan", + url="https://account.nabucasa.com/", + ) + + await self.wait_task + + if self._data is None or self._data["provider"] == "legacy": + # If we get here we waited too long. + return self.async_external_step_done(next_step_id="timeout") + + return self.async_external_step_done(next_step_id="complete") + + async def async_step_complete(self, _: None = None) -> FlowResult: + """Handle the final step of a fix flow.""" + return self.async_create_entry(title="", data={}) + + async def async_step_timeout(self, _: None = None) -> FlowResult: + """Handle the final step of a fix flow.""" + return self.async_abort(reason="operation_took_too_long") + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + return LegacySubscriptionRepairFlow() diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index a799a8cee59..e437fca9ed3 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -13,5 +13,20 @@ "logged_in": "Logged In", "subscription_expiration": "Subscription Expiration" } + }, + "issues": { + "legacy_subscription": { + "title": "Legacy subscription detected", + "fix_flow": { + "step": { + "confirm_change_plan": { + "description": "We've recently updated our subscription system. To continue using Home Assistant Cloud you need to one-time approve the change in PayPal.\n\nThis takes 1 minute and will not increase the price." + } + }, + "abort": { + "operation_took_too_long": "The operation took too long. Please try again later." + } + } + } } } diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py new file mode 100644 index 00000000000..9a2e5bd87cf --- /dev/null +++ b/homeassistant/components/cloud/subscription.py @@ -0,0 +1,24 @@ +"""Subscription information.""" +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp.client_exceptions import ClientError +import async_timeout +from hass_nabucasa import Cloud, cloud_api + +from .const import REQUEST_TIMEOUT + +_LOGGER = logging.getLogger(__name__) + + +async def async_subscription_info(cloud: Cloud) -> dict[str, Any] | None: + """Fetch the subscription info.""" + try: + async with async_timeout.timeout(REQUEST_TIMEOUT): + return await cloud_api.async_subscription_info(cloud) + except ClientError: + _LOGGER.error("Failed to fetch subscription information") + + return None diff --git a/homeassistant/components/cloud/system_health.py b/homeassistant/components/cloud/system_health.py index 4d8a6eab64c..9f836114b3e 100644 --- a/homeassistant/components/cloud/system_health.py +++ b/homeassistant/components/cloud/system_health.py @@ -1,6 +1,5 @@ """Provide info to system health.""" from hass_nabucasa import Cloud -from yarl import URL from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback @@ -36,14 +35,14 @@ async def system_health_info(hass): data["remote_server"] = cloud.remote.snitun_server data["can_reach_cert_server"] = system_health.async_check_can_reach_url( - hass, cloud.acme_directory_server + hass, f"https://{cloud.acme_server}/directory" ) data["can_reach_cloud_auth"] = system_health.async_check_can_reach_url( hass, f"https://cognito-idp.{cloud.region}.amazonaws.com/{cloud.user_pool_id}/.well-known/jwks.json", ) data["can_reach_cloud"] = system_health.async_check_can_reach_url( - hass, URL(cloud.relayer).with_scheme("https").with_path("/status") + hass, f"https://{cloud.relayer_server}/status" ) return data diff --git a/homeassistant/components/cloud/translations/de.json b/homeassistant/components/cloud/translations/de.json index 20ac7ff0fab..4528ab9bd67 100644 --- a/homeassistant/components/cloud/translations/de.json +++ b/homeassistant/components/cloud/translations/de.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "Der Vorgang hat zu lange gedauert. Bitte versuche es sp\u00e4ter noch einmal." + }, + "step": { + "confirm_change_plan": { + "description": "Wir haben k\u00fcrzlich unser Abonnementsystem aktualisiert. Um die Home Assistant Cloud weiterhin nutzen zu k\u00f6nnen, musst du die \u00c4nderung einmalig in PayPal genehmigen. \n\nDies dauert 1 Minute und erh\u00f6ht den Preis nicht." + } + } + }, + "title": "Legacy Abonnement erkannt" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa aktiviert", diff --git a/homeassistant/components/cloud/translations/en.json b/homeassistant/components/cloud/translations/en.json index 7577a9a51e4..fa7376f80c8 100644 --- a/homeassistant/components/cloud/translations/en.json +++ b/homeassistant/components/cloud/translations/en.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "The operation took too long. Please try again later." + }, + "step": { + "confirm_change_plan": { + "description": "We've recently updated our subscription system. To continue using Home Assistant Cloud you need to one-time approve the change in PayPal.\n\nThis takes 1 minute and will not increase the price." + } + } + }, + "title": "Legacy subscription detected" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa Enabled", diff --git a/homeassistant/components/cloud/translations/es.json b/homeassistant/components/cloud/translations/es.json index 7eddc5c4109..6789dc4b967 100644 --- a/homeassistant/components/cloud/translations/es.json +++ b/homeassistant/components/cloud/translations/es.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "La operaci\u00f3n ha tardado demasiado. Por favor, int\u00e9ntalo de nuevo m\u00e1s tarde." + }, + "step": { + "confirm_change_plan": { + "description": "Recientemente hemos actualizado nuestro sistema de suscripci\u00f3n. Para continuar usando Home Assistant Cloud, debes aprobar el cambio una sola vez en PayPal. \n\nEsto toma 1 minuto y no aumentar\u00e1 el precio." + } + } + }, + "title": "Suscripci\u00f3n heredada detectada" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa habilitada", diff --git a/homeassistant/components/cloud/translations/et.json b/homeassistant/components/cloud/translations/et.json index 59c2b8c6e82..f4201ed0c34 100644 --- a/homeassistant/components/cloud/translations/et.json +++ b/homeassistant/components/cloud/translations/et.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "Toiming v\u00f5ttis liiga kaua aega. Palun proovi hiljem uuesti." + }, + "step": { + "confirm_change_plan": { + "description": "Oleme hiljuti uuendanud oma tellimiss\u00fcsteemi. Home Assistant Cloudi kasutamise j\u00e4tkamiseks pead \u00fchekordselt kinnitama muudatuse PayPalis.\n\nSee v\u00f5tab aega 1 minut ja ei t\u00f5sta hinda." + } + } + }, + "title": "Tuvastati p\u00e4randtellimus" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa on lubatud", diff --git a/homeassistant/components/cloud/translations/id.json b/homeassistant/components/cloud/translations/id.json index a8f6d7b4b67..2b1f105d08c 100644 --- a/homeassistant/components/cloud/translations/id.json +++ b/homeassistant/components/cloud/translations/id.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "Operasi terlalu lama. Coba lagi nanti." + }, + "step": { + "confirm_change_plan": { + "description": "Kami baru saja memperbarui sistem langganan kami. Untuk terus menggunakan Home Assistant Cloud, Anda perlu menyetujui perubahan tersebut satu kali di PayPal.\n\nIni membutuhkan waktu 1 menit dan tidak akan menaikkan harga." + } + } + }, + "title": "Langganan lawas terdeteksi" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa Diaktifkan", diff --git a/homeassistant/components/cloud/translations/no.json b/homeassistant/components/cloud/translations/no.json index e3ae7a4f766..c3a08f2d1f3 100644 --- a/homeassistant/components/cloud/translations/no.json +++ b/homeassistant/components/cloud/translations/no.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "Operasjonen tok for lang tid. Pr\u00f8v igjen senere." + }, + "step": { + "confirm_change_plan": { + "description": "Vi har nylig oppdatert abonnementssystemet v\u00e5rt. For \u00e5 fortsette \u00e5 bruke Home Assistant Cloud m\u00e5 du en gang godkjenne endringen i PayPal. \n\n Dette tar 1 minutt og vil ikke \u00f8ke prisen." + } + } + }, + "title": "Eldre abonnement oppdaget" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa aktivert", diff --git a/homeassistant/components/cloud/translations/pt-BR.json b/homeassistant/components/cloud/translations/pt-BR.json index 7e9a1f71c06..4723adf6048 100644 --- a/homeassistant/components/cloud/translations/pt-BR.json +++ b/homeassistant/components/cloud/translations/pt-BR.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "A opera\u00e7\u00e3o demorou muito. Por favor, tente novamente mais tarde." + }, + "step": { + "confirm_change_plan": { + "description": "Recentemente, atualizamos nosso sistema de assinatura. Para continuar usando o Home Assistant Cloud, voc\u00ea precisa aprovar uma vez a altera\u00e7\u00e3o no PayPal. \n\n Isso leva 1 minuto e n\u00e3o aumentar\u00e1 o pre\u00e7o." + } + } + }, + "title": "Assinatura herdada detectada" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa habilitada", diff --git a/homeassistant/components/cloud/translations/ru.json b/homeassistant/components/cloud/translations/ru.json index aa3c34ad700..ac57b0ca750 100644 --- a/homeassistant/components/cloud/translations/ru.json +++ b/homeassistant/components/cloud/translations/ru.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u0438\u043b\u0430\u0441\u044c \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043e\u043b\u0433\u043e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0437\u0436\u0435." + }, + "step": { + "confirm_change_plan": { + "description": "\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u043c\u044b \u043e\u0431\u043d\u043e\u0432\u0438\u043b\u0438 \u043d\u0430\u0448\u0443 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438. \u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 Home Assistant Cloud, \u0412\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0432 PayPal. \n\n\u042d\u0442\u043e \u0437\u0430\u0439\u043c\u0451\u0442 1 \u043c\u0438\u043d\u0443\u0442\u0443 \u0438 \u043d\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0451\u0442 \u043a \u0443\u0432\u0435\u043b\u0438\u0447\u0435\u043d\u0438\u044e \u0446\u0435\u043d\u044b." + } + } + }, + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0430\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430" + } + }, "system_health": { "info": { "alexa_enabled": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 Alexa", diff --git a/homeassistant/components/cloud/translations/sk.json b/homeassistant/components/cloud/translations/sk.json new file mode 100644 index 00000000000..f96b0cacfc1 --- /dev/null +++ b/homeassistant/components/cloud/translations/sk.json @@ -0,0 +1,11 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa povolen\u00e1", + "can_reach_cert_server": "Dosiahnutie servera certifik\u00e1tov", + "google_enabled": "Google povolen\u00e9", + "remote_connected": "Vzdialene pripojen\u00e9", + "remote_enabled": "Povolen\u00e9 vzdialen\u00e9 ovl\u00e1danie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/zh-Hant.json b/homeassistant/components/cloud/translations/zh-Hant.json index 619b0dde71c..0fbfd6d00bf 100644 --- a/homeassistant/components/cloud/translations/zh-Hant.json +++ b/homeassistant/components/cloud/translations/zh-Hant.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "\u4f5c\u696d\u8017\u6642\u904e\u4e45\u3001\u8acb\u7a0d\u5019\u518d\u8a66\u3002" + }, + "step": { + "confirm_change_plan": { + "description": "\u6211\u5011\u6700\u8fd1\u66f4\u65b0\u4e86\u8a02\u95b1\u7cfb\u7d71\u3001\u6b32\u7e7c\u7e8c\u4f7f\u7528 Home Assistant \u96f2\u670d\u52d9\u3001\u9700\u8981\u91cd\u65b0\u540c\u610f PayPal \u4e0a\u7684\u8b8a\u66f4\u3002\n\n\u5927\u6982\u9700\u8981 1 \u5206\u9418\u3001\u540c\u6642\u4e0d\u6703\u589e\u52a0\u4efb\u4f55\u8cbb\u7528\u3002" + } + } + }, + "title": "\u767c\u73fe\u820a\u8a02\u95b1\u6a21\u5f0f" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa \u5df2\u555f\u7528", diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index df5b08e9395..3a8f6b39ae7 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging +from aiohttp import ClientSession from pycfdns import CloudflareUpdater from pycfdns.exceptions import ( CloudflareAuthenticationException, @@ -14,10 +15,16 @@ from pycfdns.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.location import async_detect_location_info +from homeassistant.util.network import is_ipv4_address from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE_RECORDS @@ -28,8 +35,9 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cloudflare from a config entry.""" + session = async_get_clientsession(hass) cfupdate = CloudflareUpdater( - async_get_clientsession(hass), + session, entry.data[CONF_API_TOKEN], entry.data[CONF_ZONE], entry.data[CONF_RECORDS], @@ -45,14 +53,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_records(now): """Set up recurring update.""" try: - await _async_update_cloudflare(cfupdate, zone_id) + await _async_update_cloudflare(session, cfupdate, zone_id) except CloudflareException as error: _LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error) async def update_records_service(call: ServiceCall) -> None: """Set up service for manual trigger.""" try: - await _async_update_cloudflare(cfupdate, zone_id) + await _async_update_cloudflare(session, cfupdate, zone_id) except CloudflareException as error: _LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error) @@ -76,11 +84,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _async_update_cloudflare(cfupdate: CloudflareUpdater, zone_id: str): +async def _async_update_cloudflare( + session: ClientSession, + cfupdate: CloudflareUpdater, + zone_id: str, +) -> None: _LOGGER.debug("Starting update for zone %s", cfupdate.zone) records = await cfupdate.get_record_info(zone_id) _LOGGER.debug("Records: %s", records) - await cfupdate.update_records(zone_id, records) + location_info = await async_detect_location_info(session) + + if not location_info or not is_ipv4_address(location_info.ip): + raise HomeAssistantError("Could not get external IPv4 address") + + await cfupdate.update_records(zone_id, records, location_info.ip) _LOGGER.debug("Update for zone %s is complete", cfupdate.zone) diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index 73b83c24cce..b1d62bb2813 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -2,7 +2,7 @@ "domain": "cloudflare", "name": "Cloudflare", "documentation": "https://www.home-assistant.io/integrations/cloudflare", - "requirements": ["pycfdns==1.2.2"], + "requirements": ["pycfdns==2.0.1"], "codeowners": ["@ludeeus", "@ctalkington"], "config_flow": true, "iot_class": "cloud_push", diff --git a/homeassistant/components/cloudflare/translations/bg.json b/homeassistant/components/cloudflare/translations/bg.json index 84593a40a00..7690840a21a 100644 --- a/homeassistant/components/cloudflare/translations/bg.json +++ b/homeassistant/components/cloudflare/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/cloudflare/translations/sk.json b/homeassistant/components/cloudflare/translations/sk.json index 4af875cd1ab..96d8f8061e1 100644 --- a/homeassistant/components/cloudflare/translations/sk.json +++ b/homeassistant/components/cloudflare/translations/sk.json @@ -2,6 +2,7 @@ "config": { "abort": { "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { diff --git a/homeassistant/components/co2signal/translations/de.json b/homeassistant/components/co2signal/translations/de.json index e35b991566f..f88316ba6fc 100644 --- a/homeassistant/components/co2signal/translations/de.json +++ b/homeassistant/components/co2signal/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "api_ratelimit": "API Ratelimit \u00fcberschritten", + "api_ratelimit": "API Ratenlimit \u00fcberschritten", "unknown": "Unerwarteter Fehler" }, "error": { diff --git a/homeassistant/components/coinbase/translations/bg.json b/homeassistant/components/coinbase/translations/bg.json index cce4b6f5c2a..eb72ab1d10d 100644 --- a/homeassistant/components/coinbase/translations/bg.json +++ b/homeassistant/components/coinbase/translations/bg.json @@ -16,12 +16,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Coinbase \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", - "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Coinbase \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" - } - }, "options": { "error": { "currency_unavailable": "\u0415\u0434\u043d\u043e \u0438\u043b\u0438 \u043f\u043e\u0432\u0435\u0447\u0435 \u043e\u0442 \u0438\u0441\u043a\u0430\u043d\u0438\u0442\u0435 \u0432\u0430\u043b\u0443\u0442\u043d\u0438 \u0441\u0430\u043b\u0434\u0430 \u043d\u0435 \u0441\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0442 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f Coinbase API.", diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json index a545f8a278f..116b611f272 100644 --- a/homeassistant/components/coinbase/translations/ca.json +++ b/homeassistant/components/coinbase/translations/ca.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "La configuraci\u00f3 de Coinbase mitjan\u00e7ant YAML s'ha eliminat.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "La configuraci\u00f3 YAML de Coinbase s'ha eliminat" - } - }, "options": { "error": { "currency_unavailable": "L'API de Coinbase no proporciona algun/s dels saldos de moneda que has sol\u00b7licitat.", diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json index 7ec64b2fa14..3209d809966 100644 --- a/homeassistant/components/coinbase/translations/cs.json +++ b/homeassistant/components/coinbase/translations/cs.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_auth_key": "P\u0159ihla\u0161ovac\u00ed \u00fadaje API zam\u00edtnuty Coinbasem z d\u016fvodu neplatn\u00e9ho kl\u00ed\u010de API.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index 8f91208e58f..60518f0140c 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "Die Konfiguration von Coinbase mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Coinbase YAML-Konfiguration wurde entfernt" - } - }, "options": { "error": { "currency_unavailable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von deiner Coinbase-API nicht bereitgestellt.", @@ -36,12 +30,12 @@ "step": { "init": { "data": { - "account_balance_currencies": "Zu meldende Wallet-Guthaben.", + "account_balance_currencies": "Zu meldendes Wallet-Guthaben.", "exchange_base": "Basisw\u00e4hrung f\u00fcr Wechselkurssensoren.", "exchange_rate_currencies": "Zu meldende Wechselkurse.", "exchnage_rate_precision": "Anzahl der Dezimalstellen f\u00fcr Wechselkurse." }, - "description": "Coinbase-Optionen anpassen" + "description": "Coinbase Optionen anpassen" } } } diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index d0df4dee967..8d89b9b546b 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "Se ha eliminado la configuraci\u00f3n de Coinbase mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se ha eliminado la configuraci\u00f3n YAML de Coinbase" - } - }, "options": { "error": { "currency_unavailable": "Tu API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json index d91201b3eb0..14bd1eea370 100644 --- a/homeassistant/components/coinbase/translations/et.json +++ b/homeassistant/components/coinbase/translations/et.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "Coinbase'i seadistamine YAML-i abil on eemaldatud.\n\nHome Assistant ei kasuta olemasolevat YAML-i konfiguratsiooni.\n\nEemalda sidumine failist configuration.yaml ja taask\u00e4ivita selle probleemi lahendamiseks Home Assistant.", - "title": "Coinbase YAML konfiguratsioon on eemaldatud" - } - }, "options": { "error": { "currency_unavailable": "Coinbase API ei paku \u00fchte v\u00f5i mitut soovitud valuutasaldot.", diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json index 3eee97475f3..54122d29966 100644 --- a/homeassistant/components/coinbase/translations/hu.json +++ b/homeassistant/components/coinbase/translations/hu.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "A Coinbase YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Coinbase YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" - } - }, "options": { "error": { "currency_unavailable": "A k\u00e9rt valutaegyenlegek k\u00f6z\u00fcl egyet vagy t\u00f6bbet nem biztos\u00edt a Coinbase API.", diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json index 7cb9c5c4992..114c69acce2 100644 --- a/homeassistant/components/coinbase/translations/id.json +++ b/homeassistant/components/coinbase/translations/id.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "Proses konfigurasi Integrasi Coinbase lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Integrasi Coinbase telah dihapus" - } - }, "options": { "error": { "currency_unavailable": "Satu atau beberapa saldo mata uang yang diminta tidak disediakan oleh API Coinbase Anda.", diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json index 06507e2e71c..f26e08a727c 100644 --- a/homeassistant/components/coinbase/translations/it.json +++ b/homeassistant/components/coinbase/translations/it.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "La configurazione di Coinbase tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Coinbase \u00e8 stata rimossa" - } - }, "options": { "error": { "currency_unavailable": "Uno o pi\u00f9 dei saldi in valuta richiesti non sono forniti dalla tua API Coinbase.", diff --git a/homeassistant/components/coinbase/translations/nb.json b/homeassistant/components/coinbase/translations/nb.json index 4209449f49b..42a62fb5164 100644 --- a/homeassistant/components/coinbase/translations/nb.json +++ b/homeassistant/components/coinbase/translations/nb.json @@ -4,11 +4,6 @@ "unknown": "Uventet feil" } }, - "issues": { - "removed_yaml": { - "description": "Konfigurering av Coinbase med YAML er fjernet.\n\nDin eksisterende YAML-konfigurasjon brukes ikke av Home Assistant.\n\nFjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet." - } - }, "options": { "error": { "unknown": "Uventet feil" diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index 5c47abfebbd..472a15659c0 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -21,11 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "title": "De Coinbase YAML-configuratie is verwijderd" - } - }, "options": { "error": { "currency_unavailable": "Een of meer van de gevraagde valutabalansen wordt niet geleverd door uw Coinbase API.", diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json index 1cac2a2e741..c3f2b34cf92 100644 --- a/homeassistant/components/coinbase/translations/no.json +++ b/homeassistant/components/coinbase/translations/no.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "Konfigurering av Coinbase med YAML er fjernet.\n\nDin eksisterende YAML-konfigurasjon brukes ikke av Home Assistant.\n\nFjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet.", - "title": "Coinbase YAML-konfigurasjonen er fjernet" - } - }, "options": { "error": { "currency_unavailable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.", diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json index 3b81c0f7aca..70a1a021cdf 100644 --- a/homeassistant/components/coinbase/translations/pl.json +++ b/homeassistant/components/coinbase/translations/pl.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "Konfiguracja Coinbase za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", - "title": "Konfiguracja YAML dla Coinbase zosta\u0142a usuni\u0119ta" - } - }, "options": { "error": { "currency_unavailable": "Jeden lub wi\u0119cej \u017c\u0105danych sald walutowych nie jest dostarczanych przez interfejs API Coinbase.", diff --git a/homeassistant/components/coinbase/translations/pt-BR.json b/homeassistant/components/coinbase/translations/pt-BR.json index 6a8afe42dba..5f2bb7d96e3 100644 --- a/homeassistant/components/coinbase/translations/pt-BR.json +++ b/homeassistant/components/coinbase/translations/pt-BR.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "A configura\u00e7\u00e3o do Coinbase usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o YAML do Coinbase foi removida" - } - }, "options": { "error": { "currency_unavailable": "Um ou mais dos saldos de moeda solicitados n\u00e3o s\u00e3o fornecidos pela sua API Coinbase.", diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json index 85ec5646749..cbdf39e61a6 100644 --- a/homeassistant/components/coinbase/translations/ru.json +++ b/homeassistant/components/coinbase/translations/ru.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Coinbase\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Coinbase \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" - } - }, "options": { "error": { "currency_unavailable": "\u041e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0441\u0442\u0430\u0442\u043a\u043e\u0432 \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0412\u0430\u0448\u0438\u043c API Coinbase.", diff --git a/homeassistant/components/coinbase/translations/sk.json b/homeassistant/components/coinbase/translations/sk.json index f161003be52..ec8f33d2d7b 100644 --- a/homeassistant/components/coinbase/translations/sk.json +++ b/homeassistant/components/coinbase/translations/sk.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_auth_key": "Poverenia API zamietnut\u00e9 spolo\u010dnos\u0165ou Coinbase z d\u00f4vodu neplatn\u00e9ho k\u013e\u00fa\u010da API.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { @@ -13,7 +19,8 @@ }, "options": { "error": { - "currency_unavailable": "Jeden alebo viacero po\u017eadovan\u00fdch zostatkov mien nie s\u00fa poskytovan\u00e9 Va\u0161\u00edm Coinbase API." + "currency_unavailable": "Jeden alebo viacero po\u017eadovan\u00fdch zostatkov mien nie s\u00fa poskytovan\u00e9 Va\u0161\u00edm Coinbase API.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" } } } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/tr.json b/homeassistant/components/coinbase/translations/tr.json index ca91c29200e..b84e2bf740e 100644 --- a/homeassistant/components/coinbase/translations/tr.json +++ b/homeassistant/components/coinbase/translations/tr.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "Coinbase'i YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "Coinbase YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" - } - }, "options": { "error": { "currency_unavailable": "\u0130stenen para birimi bakiyelerinden biri veya daha fazlas\u0131 Coinbase API'niz taraf\u0131ndan sa\u011flanm\u0131yor.", diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index 2b73d75d9bc..ea48d90fc7e 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -21,12 +21,6 @@ } } }, - "issues": { - "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Coinbase \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Coinbase YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" - } - }, "options": { "error": { "currency_unavailable": "Coinbase API \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u8ca8\u5e63\u9918\u984d\u3002", diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index b2c8b29478a..f4a3a29f29f 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ( @@ -64,7 +65,7 @@ def setup_platform( command: str = config[CONF_COMMAND] payload_off: str = config[CONF_PAYLOAD_OFF] payload_on: str = config[CONF_PAYLOAD_ON] - device_class: str | None = config.get(CONF_DEVICE_CLASS) + device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS) value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) command_timeout: int = config[CONF_COMMAND_TIMEOUT] unique_id: str | None = config.get(CONF_UNIQUE_ID) @@ -95,7 +96,7 @@ class CommandBinarySensor(BinarySensorEntity): self, data: CommandSensorData, name: str, - device_class: str | None, + device_class: BinarySensorDeviceClass | None, payload_on: str, payload_off: str, value_template: Template | None, diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index c748395e95f..43bce39082d 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -49,6 +49,8 @@ class CheckConfigView(HomeAssistantView): vol.Optional("external_url"): vol.Any(cv.url_no_path, None), vol.Optional("internal_url"): vol.Any(cv.url_no_path, None), vol.Optional("currency"): cv.currency, + vol.Optional("country"): cv.country, + vol.Optional("language"): cv.language, } ) @websocket_api.async_response @@ -108,4 +110,7 @@ async def websocket_detect_config( if location_info.currency: info["currency"] = location_info.currency + if location_info.country_code: + info["country"] = location_info.country_code + connection.send_result(msg["id"], info) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 862c8c46f4d..befbfd052af 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -15,9 +15,8 @@ async def async_setup(hass): async def hook(action, config_key): """post_write_hook for Config View that reloads scenes.""" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) - if action != ACTION_DELETE: + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) return ent_reg = entity_registry.async_get(hass) diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 45a7a6dc227..73f89aaf509 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -16,9 +16,8 @@ async def async_setup(hass): async def hook(action, config_key): """post_write_hook for Config View that reloads scripts.""" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) - if action != ACTION_DELETE: + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) return ent_reg = er.async_get(hass) diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index 539547a79f1..1f738803c2e 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -192,11 +192,11 @@ class Control4Light(Control4Entity, LightEntity): return None @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag supported features.""" if self._is_dimmer: return LightEntityFeature.TRANSITION - return 0 + return LightEntityFeature(0) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" diff --git a/homeassistant/components/control4/translations/sk.json b/homeassistant/components/control4/translations/sk.json index 5ada995aa6e..29f13888b39 100644 --- a/homeassistant/components/control4/translations/sk.json +++ b/homeassistant/components/control4/translations/sk.json @@ -1,7 +1,31 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "IP adresa", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte \u00fadaje o svojom \u00fa\u010dte Control4 a IP adresu miestneho kontrol\u00e9ra." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekundy medzi aktualiz\u00e1ciami" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/sk.json b/homeassistant/components/coolmaster/translations/sk.json new file mode 100644 index 00000000000..4bd1fb9cf1d --- /dev/null +++ b/homeassistant/components/coolmaster/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "cool": "Podpora chladiaceho re\u017eimu", + "dry": "Podpora su\u0161iaceho re\u017eimu", + "fan_only": "Podpora re\u017eimu iba ventil\u00e1tora", + "heat": "Podpora re\u017eimu vykurovania", + "heat_cool": "Podpora automatick\u00e9ho re\u017eimu vykurovania/chladenia", + "host": "Hostite\u013e" + }, + "title": "Nastavte podrobnosti pripojenia CoolMasterNet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/sk.json b/homeassistant/components/coronavirus/translations/sk.json new file mode 100644 index 00000000000..c9e214a81d2 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "country": "Krajina" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 57187c56819..072710aa947 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta -from enum import IntEnum +from enum import IntFlag import functools as ft import logging from typing import Any, TypeVar, final @@ -86,7 +86,7 @@ DEVICE_CLASS_WINDOW = CoverDeviceClass.WINDOW.value # mypy: disallow-any-generics -class CoverEntityFeature(IntEnum): +class CoverEntityFeature(IntFlag): """Supported features of the cover entity.""" OPEN = 1 @@ -232,6 +232,7 @@ class CoverEntity(Entity): _attr_is_closing: bool | None = None _attr_is_opening: bool | None = None _attr_state: None = None + _attr_supported_features: CoverEntityFeature | None _cover_is_last_toggle_direction_open = True @@ -291,7 +292,7 @@ class CoverEntity(Entity): return data @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" if self._attr_supported_features is not None: return self._attr_supported_features diff --git a/homeassistant/components/cover/translations/de.json b/homeassistant/components/cover/translations/de.json index bf320e07f9e..81fb1fb211c 100644 --- a/homeassistant/components/cover/translations/de.json +++ b/homeassistant/components/cover/translations/de.json @@ -14,8 +14,8 @@ "is_closing": "{entity_name} wird geschlossen", "is_open": "{entity_name} ist offen", "is_opening": "{entity_name} wird ge\u00f6ffnet", - "is_position": "Die Aktuelle Position von {entity_name} ist", - "is_tilt_position": "Die Aktuelle Neigungsposition von {entity_name} ist" + "is_position": "Die aktuelle Position von {entity_name} ist", + "is_tilt_position": "Die aktuelle Neigungsposition von {entity_name} ist" }, "trigger_type": { "closed": "{entity_name} geschlossen", diff --git a/homeassistant/components/cover/translations/is.json b/homeassistant/components/cover/translations/is.json index ce8052eb02b..f3844b33c61 100644 --- a/homeassistant/components/cover/translations/is.json +++ b/homeassistant/components/cover/translations/is.json @@ -1,7 +1,10 @@ { "device_automation": { "condition_type": { - "is_closed": "{entity_name} er loku\u00f0" + "is_closed": "{entity_name} er loku\u00f0", + "is_closing": "{entity_name} er a\u00f0 loka", + "is_open": "{entity_name} er opin", + "is_opening": "{entity_name} er a\u00f0 opnast" }, "trigger_type": { "closed": "{entity_name} loku\u00f0", diff --git a/homeassistant/components/cover/translations/sk.json b/homeassistant/components/cover/translations/sk.json index 57379849b32..335ab691980 100644 --- a/homeassistant/components/cover/translations/sk.json +++ b/homeassistant/components/cover/translations/sk.json @@ -1,4 +1,26 @@ { + "device_automation": { + "action_type": { + "close": "Zavrie\u0165 {entity_name}", + "open": "Otvori\u0165 {entity_name}", + "set_position": "Nastavi\u0165 poz\u00edciu {entity_name}", + "stop": "Zastavi\u0165 {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} je zatvoren\u00e9", + "is_closing": "{entity_name} sa zatv\u00e1ra", + "is_open": "{entity_name} je otvoren\u00e1", + "is_opening": "{entity_name} sa otv\u00e1ra", + "is_position": "Aktu\u00e1lna poz\u00edcia {entity_name} je", + "is_tilt_position": "Aktu\u00e1lna poloha naklonenia {entity_name} je" + }, + "trigger_type": { + "closed": "{entity_name} zatvoren\u00e9", + "closing": "zatv\u00e1renie {entity_name}", + "opened": "{entity_name} otvoren\u00e9", + "position": "zmeny poz\u00edcie {entity_name}" + } + }, "state": { "_": { "closed": "Zatvoren\u00e9", diff --git a/homeassistant/components/cpuspeed/translations/sk.json b/homeassistant/components/cpuspeed/translations/sk.json new file mode 100644 index 00000000000..8b80c1ddb35 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "not_compatible": "Nie je mo\u017en\u00e9 z\u00edska\u0165 inform\u00e1cie o CPU, t\u00e1to integr\u00e1cia nie je kompatibiln\u00e1 s va\u0161\u00edm syst\u00e9mom" + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?", + "title": "R\u00fdchlos\u0165 CPU" + } + } + }, + "title": "R\u00fdchlos\u0165 CPU" +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/de.json b/homeassistant/components/crownstone/translations/de.json index 054d6987c29..1719fa9ace7 100644 --- a/homeassistant/components/crownstone/translations/de.json +++ b/homeassistant/components/crownstone/translations/de.json @@ -15,8 +15,8 @@ "data": { "usb_path": "USB-Ger\u00e4te-Pfad" }, - "description": "W\u00e4hle den seriellen Anschluss des Crownstone-USB-Dongles aus, oder w\u00e4hle \"Don't use USB\", wenn du keinen USB-Dongle einrichten m\u00f6chtest.\n\nSuche nach einem Ger\u00e4t mit VID 10C4 und PID EA60.", - "title": "Crownstone USB-Dongle-Konfiguration" + "description": "W\u00e4hle den seriellen Anschluss des Crownstone USB-Dongles aus oder w\u00e4hle \"Don't use USB\", wenn du keinen USB-Dongle einrichten m\u00f6chtest.\n\nSuche nach einem Ger\u00e4t mit VID 10C4 und PID EA60.", + "title": "Crownstone USB-Dongle Konfiguration" }, "usb_manual_config": { "data": { @@ -37,7 +37,7 @@ "email": "E-Mail", "password": "Passwort" }, - "title": "Crownstone-Konto" + "title": "Crownstone Konto" } } }, @@ -53,8 +53,8 @@ "data": { "usb_path": "USB-Ger\u00e4te-Pfad" }, - "description": "W\u00e4hle den seriellen Anschluss des Crownstone-USB-Dongles.\n\nSuche nach einem Ger\u00e4t mit VID 10C4 und PID EA60.", - "title": "Crownstone USB-Dongle-Konfiguration" + "description": "W\u00e4hle den seriellen Anschluss des Crownstone USB-Dongles.\n\nSuche nach einem Ger\u00e4t mit VID 10C4 und PID EA60.", + "title": "Crownstone USB-Dongle Konfiguration" }, "usb_manual_config": { "data": { diff --git a/homeassistant/components/crownstone/translations/sk.json b/homeassistant/components/crownstone/translations/sk.json index 72b0304f1c3..3c0ec2452c2 100644 --- a/homeassistant/components/crownstone/translations/sk.json +++ b/homeassistant/components/crownstone/translations/sk.json @@ -1,12 +1,42 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "account_not_verified": "\u00da\u010det nie je overen\u00fd. Aktivujte si svoj \u00fa\u010det prostredn\u00edctvom aktiva\u010dn\u00e9ho e-mailu od Crownstone.", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { + "usb_config": { + "data": { + "usb_path": "Cesta k zariadeniu USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Cesta k zariadeniu USB" + } + }, "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" + } + } + } + }, + "options": { + "step": { + "usb_config": { + "data": { + "usb_path": "Cesta k zariadeniu USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Cesta k zariadeniu USB" } } } diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 02107205e70..bd4763d3254 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -116,6 +116,8 @@ def format_target_temperature(target_temperature): class DaikinClimate(ClimateEntity): """Representation of a Daikin HVAC.""" + _attr_name = None + _attr_has_entity_name = True _attr_temperature_unit = TEMP_CELSIUS def __init__(self, api: DaikinApi) -> None: @@ -173,11 +175,6 @@ class DaikinClimate(ClimateEntity): if values: await self._api.device.set(values) - @property - def name(self): - """Return the name of the thermostat, if any.""" - return self._api.name - @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index fbd838b9e47..a978e96178d 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -4,12 +4,17 @@ DOMAIN = "daikin" ATTR_TARGET_TEMPERATURE = "target_temperature" ATTR_INSIDE_TEMPERATURE = "inside_temperature" ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" -ATTR_TOTAL_POWER = "total_power" + +ATTR_TARGET_HUMIDITY = "target_humidity" +ATTR_HUMIDITY = "humidity" + +ATTR_COMPRESSOR_FREQUENCY = "compressor_frequency" + +ATTR_ENERGY_TODAY = "energy_today" ATTR_COOL_ENERGY = "cool_energy" ATTR_HEAT_ENERGY = "heat_energy" -ATTR_HUMIDITY = "humidity" -ATTR_TARGET_HUMIDITY = "target_humidity" -ATTR_COMPRESSOR_FREQUENCY = "compressor_frequency" + +ATTR_TOTAL_POWER = "total_power" ATTR_TOTAL_ENERGY_TODAY = "total_energy_today" ATTR_STATE_ON = "on" diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 1adacd322cc..3d9deba59ab 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -29,6 +29,7 @@ from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi from .const import ( ATTR_COMPRESSOR_FREQUENCY, ATTR_COOL_ENERGY, + ATTR_ENERGY_TODAY, ATTR_HEAT_ENERGY, ATTR_HUMIDITY, ATTR_INSIDE_TEMPERATURE, @@ -54,7 +55,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( DaikinSensorEntityDescription( key=ATTR_INSIDE_TEMPERATURE, - name="Inside Temperature", + name="Inside temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, @@ -62,7 +63,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), DaikinSensorEntityDescription( key=ATTR_OUTSIDE_TEMPERATURE, - name="Outside Temperature", + name="Outside temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, @@ -78,7 +79,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), DaikinSensorEntityDescription( key=ATTR_TARGET_HUMIDITY, - name="Target Humidity", + name="Target humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -86,7 +87,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), DaikinSensorEntityDescription( key=ATTR_TOTAL_POWER, - name="Estimated Power Consumption", + name="Compressor estimated power consumption", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -94,35 +95,47 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( ), DaikinSensorEntityDescription( key=ATTR_COOL_ENERGY, - name="Cool Energy Consumption", + name="Cool energy consumption", icon="mdi:snowflake", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + entity_registry_enabled_default=False, value_func=lambda device: round(device.last_hour_cool_energy_consumption, 2), ), DaikinSensorEntityDescription( key=ATTR_HEAT_ENERGY, - name="Heat Energy Consumption", + name="Heat energy consumption", icon="mdi:fire", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + entity_registry_enabled_default=False, value_func=lambda device: round(device.last_hour_heat_energy_consumption, 2), ), + DaikinSensorEntityDescription( + key=ATTR_ENERGY_TODAY, + name="Energy consumption", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_func=lambda device: round(device.today_energy_consumption, 2), + ), DaikinSensorEntityDescription( key=ATTR_COMPRESSOR_FREQUENCY, - name="Compressor Frequency", + name="Compressor frequency", icon="mdi:fan", device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=FREQUENCY_HERTZ, + entity_registry_enabled_default=False, value_func=lambda device: device.compressor_frequency, ), DaikinSensorEntityDescription( key=ATTR_TOTAL_ENERGY_TODAY, - name="Today's Total Energy Consumption", + name="Compressor energy consumption", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + entity_registry_enabled_default=False, value_func=lambda device: round(device.today_total_energy_consumption, 2), ), ) @@ -150,9 +163,10 @@ async def async_setup_entry( if daikin_api.device.support_outside_temperature: sensors.append(ATTR_OUTSIDE_TEMPERATURE) if daikin_api.device.support_energy_consumption: - sensors.append(ATTR_TOTAL_POWER) + sensors.append(ATTR_ENERGY_TODAY) sensors.append(ATTR_COOL_ENERGY) sensors.append(ATTR_HEAT_ENERGY) + sensors.append(ATTR_TOTAL_POWER) sensors.append(ATTR_TOTAL_ENERGY_TODAY) if daikin_api.device.support_humidity: sensors.append(ATTR_HUMIDITY) @@ -171,6 +185,7 @@ async def async_setup_entry( class DaikinSensor(SensorEntity): """Representation of a Sensor.""" + _attr_has_entity_name = True entity_description: DaikinSensorEntityDescription def __init__( @@ -179,7 +194,6 @@ class DaikinSensor(SensorEntity): """Initialize the sensor.""" self.entity_description = description self._api = api - self._attr_name = f"{api.name} {description.name}" @property def unique_id(self) -> str: diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 23b4b526f9a..a7c8b6549e4 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -56,6 +56,9 @@ async def async_setup_entry( class DaikinZoneSwitch(SwitchEntity): """Representation of a zone.""" + _attr_icon = ZONE_ICON + _attr_has_entity_name = True + def __init__(self, daikin_api: DaikinApi, zone_id): """Initialize the zone.""" self._api = daikin_api @@ -66,15 +69,10 @@ class DaikinZoneSwitch(SwitchEntity): """Return a unique ID.""" return f"{self._api.device.mac}-zone{self._zone_id}" - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ZONE_ICON - @property def name(self) -> str: """Return the name of the sensor.""" - return f"{self._api.name} {self._api.device.zones[self._zone_id][0]}" + return self._api.device.zones[self._zone_id][0] @property def is_on(self) -> bool: @@ -102,6 +100,10 @@ class DaikinZoneSwitch(SwitchEntity): class DaikinStreamerSwitch(SwitchEntity): """Streamer state.""" + _attr_icon = STREAMER_ICON + _attr_name = "Streamer" + _attr_has_entity_name = True + def __init__(self, daikin_api: DaikinApi) -> None: """Initialize streamer switch.""" self._api = daikin_api @@ -111,16 +113,6 @@ class DaikinStreamerSwitch(SwitchEntity): """Return a unique ID.""" return f"{self._api.device.mac}-streamer" - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return STREAMER_ICON - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._api.name} streamer" - @property def is_on(self) -> bool: """Return the state of the sensor.""" diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index a07f37ab8d5..4ad69dc8249 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -6,6 +6,7 @@ }, "error": { "api_password": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 API \u043a\u043b\u044e\u0447 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/daikin/translations/sk.json b/homeassistant/components/daikin/translations/sk.json index a1222826481..b0c325dc02a 100644 --- a/homeassistant/components/daikin/translations/sk.json +++ b/homeassistant/components/daikin/translations/sk.json @@ -1,14 +1,23 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "error": { "api_password": "Neplatn\u00e9 overenie, pou\u017eite bu\u010f API k\u013e\u00fa\u010d alebo heslo.", - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" - } + "api_key": "API k\u013e\u00fa\u010d", + "host": "Hostite\u013e", + "password": "Heslo" + }, + "title": "Konfigur\u00e1cia Daikin AC" } } } diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 88f3b6b2bc9..530f1788dd8 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -36,11 +36,11 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MODE, CONF_NAME, - LENGTH_KILOMETERS, - LENGTH_MILLIMETERS, - PRESSURE_MBAR, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -113,11 +113,11 @@ def setup_platform( class DarkSkyWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_MBAR - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_visibility_unit = LENGTH_KILOMETERS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.MBAR + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_visibility_unit = UnitOfLength.KILOMETERS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__(self, name, dark_sky, mode): """Initialize Dark Sky weather.""" diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 6e0c4c86d21..ef78e8f1419 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -33,6 +33,7 @@ import homeassistant.helpers.entity_registry as er from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry +from .util import serial_from_unique_id _SensorDeviceT = TypeVar("_SensorDeviceT", bound=PydeconzSensorBase) @@ -187,7 +188,9 @@ def async_update_unique_id( return if description.old_unique_id_suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' + unique_id = ( + f"{serial_from_unique_id(unique_id)}-{description.old_unique_id_suffix}" + ) if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 6070f83871f..ca38edf0625 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -57,3 +57,6 @@ POWER_PLUGS = [ CONF_ANGLE = "angle" CONF_GESTURE = "gesture" + +ATTR_DURATION = "duration" +ATTR_ROTATION = "rotation" diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index c2161baf100..6163db0dc65 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as DECONZ_DOMAIN from .gateway import DeconzGateway +from .util import serial_from_unique_id _DeviceT = TypeVar( "_DeviceT", @@ -55,9 +56,7 @@ class DeconzBase(Generic[_DeviceT]): def serial(self) -> str | None: """Return a serial number for this device.""" assert isinstance(self._device, PydeconzDevice) - if not self._device.unique_id or self._device.unique_id.count(":") != 7: - return None - return self._device.unique_id.split("-", 1)[0] + return serial_from_unique_id(self._device.unique_id) @property def device_info(self) -> DeviceInfo | None: diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 35e1ba79948..9bde87e5e17 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -10,6 +10,7 @@ from pydeconz.models.sensor.ancillary_control import ( AncillaryControlAction, ) from pydeconz.models.sensor.presence import Presence, PresenceStatePresenceEvent +from pydeconz.models.sensor.relative_rotary import RelativeRotary, RelativeRotaryEvent from pydeconz.models.sensor.switch import Switch from homeassistant.const import ( @@ -23,13 +24,14 @@ from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.util import slugify -from .const import CONF_ANGLE, CONF_GESTURE, LOGGER +from .const import ATTR_DURATION, ATTR_ROTATION, CONF_ANGLE, CONF_GESTURE, LOGGER from .deconz_device import DeconzBase from .gateway import DeconzGateway CONF_DECONZ_EVENT = "deconz_event" CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" CONF_DECONZ_PRESENCE_EVENT = "deconz_presence_event" +CONF_DECONZ_RELATIVE_ROTARY_EVENT = "deconz_relative_rotary_event" SUPPORTED_DECONZ_ALARM_EVENTS = { AncillaryControlAction.EMERGENCY, @@ -47,6 +49,10 @@ SUPPORTED_DECONZ_PRESENCE_EVENTS = { PresenceStatePresenceEvent.APPROACHING, PresenceStatePresenceEvent.ABSENTING, } +RELATIVE_ROTARY_DECONZ_TO_EVENT = { + RelativeRotaryEvent.NEW: "new", + RelativeRotaryEvent.REPEAT: "repeat", +} async def async_setup_events(gateway: DeconzGateway) -> None: @@ -55,7 +61,7 @@ async def async_setup_events(gateway: DeconzGateway) -> None: @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Create DeconzEvent.""" - new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent + new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent | DeconzRelativeRotaryEvent sensor = gateway.api.sensors[sensor_id] if isinstance(sensor, Switch): @@ -69,6 +75,9 @@ async def async_setup_events(gateway: DeconzGateway) -> None: return new_event = DeconzPresenceEvent(sensor, gateway) + elif isinstance(sensor, RelativeRotary): + new_event = DeconzRelativeRotaryEvent(sensor, gateway) + gateway.hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) @@ -84,6 +93,10 @@ async def async_setup_events(gateway: DeconzGateway) -> None: async_add_sensor, gateway.api.sensors.presence, ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors.relative_rotary, + ) @callback @@ -104,7 +117,7 @@ class DeconzEventBase(DeconzBase): def __init__( self, - device: AncillaryControl | Presence | Switch, + device: AncillaryControl | Presence | RelativeRotary | Switch, gateway: DeconzGateway, ) -> None: """Register callback that will be used for signals.""" @@ -227,3 +240,29 @@ class DeconzPresenceEvent(DeconzEventBase): } self.gateway.hass.bus.async_fire(CONF_DECONZ_PRESENCE_EVENT, data) + + +class DeconzRelativeRotaryEvent(DeconzEventBase): + """Relative rotary event.""" + + _device: RelativeRotary + + @callback + def async_update_callback(self) -> None: + """Fire the event if reason is new action is updated.""" + if ( + self.gateway.ignore_state_updates + or "rotaryevent" not in self._device.changed_keys + ): + return + + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_DEVICE_ID: self.device_id, + CONF_EVENT: RELATIVE_ROTARY_DECONZ_TO_EVENT[self._device.rotary_event], + ATTR_ROTATION: self._device.expected_rotation, + ATTR_DURATION: self._device.expected_event_duration, + } + + self.gateway.hass.bus.async_fire(CONF_DECONZ_RELATIVE_ROTARY_EVENT, data) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 8c63a47f59c..76844b026ce 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -28,6 +28,7 @@ from .deconz_event import ( DeconzAlarmEvent, DeconzEvent, DeconzPresenceEvent, + DeconzRelativeRotaryEvent, ) from .gateway import DeconzGateway @@ -635,7 +636,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( def _get_deconz_event_from_device( hass: HomeAssistant, device: dr.DeviceEntry, -) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent: +) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent | DeconzRelativeRotaryEvent: """Resolve deconz event from device.""" gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {}) for gateway in gateways.values(): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 1c381bc194a..c1b8139e6f7 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -41,7 +41,12 @@ from .const import ( from .errors import AuthenticationRequired, CannotConnect if TYPE_CHECKING: - from .deconz_event import DeconzAlarmEvent, DeconzEvent, DeconzPresenceEvent + from .deconz_event import ( + DeconzAlarmEvent, + DeconzEvent, + DeconzPresenceEvent, + DeconzRelativeRotaryEvent, + ) SENSORS = ( sensors.SensorResourceManager, @@ -93,7 +98,12 @@ class DeconzGateway: self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} - self.events: list[DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent] = [] + self.events: list[ + DeconzAlarmEvent + | DeconzEvent + | DeconzPresenceEvent + | DeconzRelativeRotaryEvent + ] = [] self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set() self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 789a155477a..154c988e07c 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -26,6 +26,7 @@ import homeassistant.helpers.entity_registry as er from .const import DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry +from .util import serial_from_unique_id T = TypeVar("T", Presence, PydeconzSensorBase) @@ -88,7 +89,7 @@ def async_update_unique_id( if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): return - unique_id = f'{unique_id.split("-", 1)[0]}-{description.key}' + unique_id = f"{serial_from_unique_id(unique_id)}-{description.key}" if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index f1bd0118030..6b47560b150 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -33,6 +33,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, + CONCENTRATION_PARTS_PER_BILLION, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, @@ -50,6 +51,7 @@ import homeassistant.util.dt as dt_util from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry +from .util import serial_from_unique_id PROVIDES_EXTRA_ATTRIBUTES = ( "battery", @@ -120,8 +122,9 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( instance_check=AirQuality, name_suffix="PPB", old_unique_id_suffix="ppb", - device_class=SensorDeviceClass.AQI, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, ), DeconzSensorDescription[Consumption]( key="consumption", @@ -248,7 +251,9 @@ def async_update_unique_id( return if description.old_unique_id_suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' + unique_id = ( + f"{serial_from_unique_id(unique_id)}-{description.old_unique_id_suffix}" + ) if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -290,7 +295,7 @@ async def async_setup_entry( sensor.type.startswith("CLIP") or (no_sensor_data and description.key != "battery") or ( - (unique_id := sensor.unique_id.rsplit("-", 1)[0]) + (unique_id := sensor.unique_id.rpartition("-")[0]) in known_device_entities[description.key] ) ): diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index b047c361681..8b9ae63a7cd 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -31,6 +31,7 @@ "device_automation": { "trigger_subtype": { "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "bottom_buttons": "\u0414\u043e\u043b\u043d\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", @@ -51,21 +52,22 @@ "side_4": "\u0421\u0442\u0440\u0430\u043d\u0430 4", "side_5": "\u0421\u0442\u0440\u0430\u043d\u0430 5", "side_6": "\u0421\u0442\u0440\u0430\u043d\u0430 6", + "top_buttons": "\u0413\u043e\u0440\u043d\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" }, "trigger_type": { "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u0435 \u0441\u044a\u0431\u0443\u0434\u0438", - "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", - "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", - "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", "remote_button_rotation_stopped": "\u0421\u043f\u0440\u044f \u0432\u044a\u0440\u0442\u0435\u043d\u0435\u0442\u043e \u043d\u0430 \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", - "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", - "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", - "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "remote_button_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \"{subtype}\" \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \u0434\u0432\u0430 \u043f\u044a\u0442\u0438", "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043f\u0430\u0434\u0430", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", diff --git a/homeassistant/components/deconz/translations/sk.json b/homeassistant/components/deconz/translations/sk.json index 81684739474..ed0cc142a44 100644 --- a/homeassistant/components/deconz/translations/sk.json +++ b/homeassistant/components/deconz/translations/sk.json @@ -1,13 +1,84 @@ { "config": { "abort": { + "already_configured": "Bridge je u\u017e nakonfigurovan\u00fd", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" }, + "error": { + "linking_not_possible": "Nepodarilo sa prepoji\u0165 s br\u00e1nou", + "no_key": "Nepodarilo sa z\u00edska\u0165 k\u013e\u00fa\u010d API" + }, + "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurova\u0165 Home Assistant na pripojenie k br\u00e1ne deCONZ poskytovanej doplnkom {addon}?" + }, + "link": { + "title": "Prepojenie s deCONZ" + }, "manual_input": { "data": { + "host": "Hostite\u013e", "port": "Port" } + }, + "user": { + "data": { + "host": "Vyberte objaven\u00fa br\u00e1nu deCONZ" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Obe tla\u010didl\u00e1", + "bottom_buttons": "Spodn\u00e9 tla\u010didl\u00e1", + "button_1": "Prv\u00e9 tla\u010didlo", + "button_2": "Druh\u00e9 tla\u010didlo", + "button_3": "Tretie tla\u010didlo", + "button_4": "\u0160tvrt\u00e9 tla\u010didlo", + "button_5": "Piate tla\u010didlo", + "button_6": "\u0160ieste tla\u010didlo", + "button_7": "Siedme tla\u010didlo", + "button_8": "\u00d4sme tla\u010didlo", + "close": "Zavrie\u0165", + "dim_down": "Stlmi\u0165", + "dim_up": "Zv\u00fd\u0161i\u0165", + "left": "V\u013eavo", + "open": "Otvori\u0165", + "right": "Vpravo", + "side_1": "Strana 1", + "side_2": "Strana 2", + "side_3": "Strana 3", + "side_4": "Strana 4", + "side_5": "Strana 5", + "side_6": "Strana 6", + "turn_off": "Vypn\u00fa\u0165", + "turn_on": "Zapn\u00fa\u0165" + }, + "trigger_type": { + "remote_awakened": "Zariadenie sa prebudilo", + "remote_button_double_press": "dvojklik na tla\u010didlo \u201e{subtype}\u201c", + "remote_button_long_press": "Trvalo stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", + "remote_button_long_release": "Tla\u010didlo \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_button_quadruple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", + "remote_button_quintuple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", + "remote_button_short_press": "Stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", + "remote_button_short_release": "Tla\u010didlo \"{subtype}\" bolo uvo\u013enen\u00e9", + "remote_button_triple_press": "Trojklik na tla\u010didlo \"{subtype}\"", + "remote_double_tap": "Zariadenie \"{subtype}\" dvojit\u00e9 klepnutie", + "remote_double_tap_any_side": "Zariadenie dvakr\u00e1t klepnut\u00e9 na \u013eubovo\u013en\u00fa stranu", + "remote_turned_clockwise": "Zariadenie oto\u010den\u00e9 v smere hodinov\u00fdch ru\u010di\u010diek", + "remote_turned_counter_clockwise": "Zariadenie oto\u010den\u00e9 proti smeru hodinov\u00fdch ru\u010di\u010diek" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "Povoli\u0165 senzory deCONZ CLIP" + }, + "title": "mo\u017enosti deCONZ" } } } diff --git a/homeassistant/components/deconz/util.py b/homeassistant/components/deconz/util.py new file mode 100644 index 00000000000..4e7b1e7739f --- /dev/null +++ b/homeassistant/components/deconz/util.py @@ -0,0 +1,9 @@ +"""Utilities for deCONZ integration.""" +from __future__ import annotations + + +def serial_from_unique_id(unique_id: str | None) -> str | None: + """Get a device serial number from a unique ID, if possible.""" + if not unique_id or unique_id.count(":") != 7: + return None + return unique_id.partition("-")[0] diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 3c43e816097..c103636563c 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -112,11 +112,11 @@ class DecoraWifiLight(LightEntity): return {self.color_mode} @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Return supported features.""" if self._switch.canSetLevel: return LightEntityFeature.TRANSITION - return 0 + return LightEntityFeature(0) @property def name(self): diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index 920e560b70f..89302d4cd48 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["deluge_client"] + "loggers": ["deluge_client"], + "integration_type": "service" } diff --git a/homeassistant/components/deluge/translations/sk.json b/homeassistant/components/deluge/translations/sk.json index 0fbba9ccd6b..d2976f03929 100644 --- a/homeassistant/components/deluge/translations/sk.json +++ b/homeassistant/components/deluge/translations/sk.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", + "password": "Heslo", "port": "Port", - "username": "U\u017e\u00edvate\u013esk\u00e9 meno" + "username": "U\u017e\u00edvate\u013esk\u00e9 meno", + "web_port": "Webov\u00fd port (pre n\u00e1v\u0161tevu slu\u017eby)" } } } diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 7ed989903e5..ae6912fa0f9 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -52,6 +52,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.TEXT, Platform.UPDATE, Platform.VACUUM, Platform.WATER_HEATER, @@ -230,6 +231,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: translation_key="bad_psu", ) + async_create_issue( + hass, + DOMAIN, + "cold_tea", + is_fixable=True, + severity=IssueSeverity.WARNING, + translation_key="cold_tea", + ) + return True @@ -267,7 +277,7 @@ async def _insert_sum_statistics( statistic_id = metadata["statistic_id"] last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, False + get_last_statistics, hass, 1, statistic_id, False, {"sum"} ) if statistic_id in last_stats: sum_ = last_stats[statistic_id][0]["sum"] or 0 diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 91594423744..00e049c6034 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN -SUPPORT_FLAGS = 0 +SUPPORT_FLAGS = ClimateEntityFeature(0) async def async_setup_platform( @@ -128,26 +128,22 @@ class DemoClimate(ClimateEntity): """Initialize the climate device.""" self._unique_id = unique_id self._attr_name = name - self._support_flags = SUPPORT_FLAGS + self._attr_supported_features = SUPPORT_FLAGS if target_temperature is not None: - self._support_flags = ( - self._support_flags | ClimateEntityFeature.TARGET_TEMPERATURE - ) + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE if preset is not None: - self._support_flags = self._support_flags | ClimateEntityFeature.PRESET_MODE + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE if fan_mode is not None: - self._support_flags = self._support_flags | ClimateEntityFeature.FAN_MODE + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE if target_humidity is not None: - self._support_flags = ( - self._support_flags | ClimateEntityFeature.TARGET_HUMIDITY - ) + self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY if swing_mode is not None: - self._support_flags = self._support_flags | ClimateEntityFeature.SWING_MODE + self._attr_supported_features |= ClimateEntityFeature.SWING_MODE if aux is not None: - self._support_flags = self._support_flags | ClimateEntityFeature.AUX_HEAT + self._attr_supported_features |= ClimateEntityFeature.AUX_HEAT if HVACMode.HEAT_COOL in hvac_modes or HVACMode.AUTO in hvac_modes: - self._support_flags = ( - self._support_flags | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) self._target_temperature = target_temperature self._target_humidity = target_humidity @@ -183,11 +179,6 @@ class DemoClimate(ClimateEntity): """Return the unique id.""" return self._unique_id - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return self._support_flags - @property def temperature_unit(self) -> str: """Return the unit of measurement.""" diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 845bd9976a3..6f443329661 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -78,7 +78,7 @@ class DemoCover(CoverEntity): position: int | None = None, tilt_position: int | None = None, device_class: CoverDeviceClass | None = None, - supported_features: int | None = None, + supported_features: CoverEntityFeature | None = None, ) -> None: """Initialize the cover.""" self.hass = hass @@ -86,7 +86,7 @@ class DemoCover(CoverEntity): self._attr_name = name self._position = position self._attr_device_class = device_class - self._supported_features = supported_features + self._attr_supported_features = supported_features self._set_position: int | None = None self._set_tilt_position: int | None = None self._tilt_position = tilt_position @@ -142,13 +142,6 @@ class DemoCover(CoverEntity): """Return if the cover is opening.""" return self._is_opening - @property - def supported_features(self) -> int: - """Flag supported features.""" - if self._supported_features is not None: - return self._supported_features - return super().supported_features - async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" if self._position == 0: diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 8dcffa6e141..5c8cc849285 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -107,13 +107,13 @@ class BaseDemoFan(FanEntity): hass: HomeAssistant, unique_id: str, name: str, - supported_features: int, + supported_features: FanEntityFeature, preset_modes: list[str] | None, ) -> None: """Initialize the entity.""" self.hass = hass self._unique_id = unique_id - self._supported_features = supported_features + self._attr_supported_features = supported_features self._percentage: int | None = None self._preset_modes = preset_modes self._preset_mode: str | None = None @@ -140,11 +140,6 @@ class BaseDemoFan(FanEntity): """Oscillating.""" return self._oscillating - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - class DemoPercentageFan(BaseDemoFan, FanEntity): """A demonstration fan component that uses percentages.""" diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index 571cfbe8db9..772726ac1d5 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -SUPPORT_FLAGS = 0 +SUPPORT_FLAGS = HumidifierEntityFeature(0) async def async_setup_platform( @@ -75,9 +75,7 @@ class DemoHumidifier(HumidifierEntity): self._attr_is_on = is_on self._attr_supported_features = SUPPORT_FLAGS if mode is not None: - self._attr_supported_features = ( - self._attr_supported_features | HumidifierEntityFeature.MODES - ) + self._attr_supported_features |= HumidifierEntityFeature.MODES self._attr_target_humidity = target_humidity self._attr_mode = mode self._attr_available_modes = available_modes diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index af8afe2c15d..2e5291b8a13 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -129,7 +129,6 @@ class DemoLight(LightEntity): self._ct = ct or random.choice(LIGHT_TEMPS) self._effect = effect self._effect_list = effect_list - self._features = 0 self._hs_color = hs_color self._attr_name = name self._rgbw_color = rgbw_color @@ -148,7 +147,7 @@ class DemoLight(LightEntity): supported_color_modes = SUPPORT_DEMO self._color_modes = supported_color_modes if self._effect_list is not None: - self._features |= LightEntityFeature.EFFECT + self._attr_supported_features |= LightEntityFeature.EFFECT @property def device_info(self) -> DeviceInfo: @@ -218,11 +217,6 @@ class DemoLight(LightEntity): """Return true if light is on.""" return self._state - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._features - @property def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index d21d89f238b..47187d5ffc6 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -109,10 +109,3 @@ class DemoLock(LockEntity): """Open the door latch.""" self._state = STATE_UNLOCKED self.async_write_ha_state() - - @property - def supported_features(self) -> int: - """Flag supported features.""" - if self._openable: - return LockEntityFeature.OPEN - return 0 diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 8bbe380d9ec..9d335c34cdb 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -105,8 +105,8 @@ NETFLIX_PLAYER_SUPPORT = ( class AbstractDemoPlayer(MediaPlayerEntity): """A demo media players.""" - _attr_sound_mode_list = SOUND_MODE_LIST _attr_should_poll = False + _attr_sound_mode_list = SOUND_MODE_LIST # We only implement the methods that we support @@ -185,48 +185,25 @@ class DemoYoutubePlayer(AbstractDemoPlayer): # We only implement the methods that we support + _attr_app_name = "YouTube" _attr_media_content_type = MediaType.MOVIE + _attr_supported_features = YOUTUBE_PLAYER_SUPPORT def __init__( self, name: str, youtube_id: str, media_title: str, duration: int ) -> None: """Initialize the demo device.""" super().__init__(name) - self.youtube_id = youtube_id - self._media_title = media_title - self._duration = duration + self._attr_media_content_id = youtube_id + self._attr_media_title = media_title + self._attr_media_duration = duration self._progress: int | None = int(duration * 0.15) self._progress_updated_at = dt_util.utcnow() - @property - def media_content_id(self) -> str: - """Return the content ID of current playing media.""" - return self.youtube_id - - @property - def media_duration(self) -> int: - """Return the duration of current playing media in seconds.""" - return self._duration - @property def media_image_url(self) -> str: """Return the image url of current playing media.""" - return f"https://img.youtube.com/vi/{self.youtube_id}/hqdefault.jpg" - - @property - def media_title(self) -> str: - """Return the title of current playing media.""" - return self._media_title - - @property - def app_name(self) -> str: - """Return the current running application.""" - return "YouTube" - - @property - def supported_features(self) -> int: - """Flag media player features that are supported.""" - return YOUTUBE_PLAYER_SUPPORT + return f"https://img.youtube.com/vi/{self.media_content_id}/hqdefault.jpg" @property def media_position(self) -> int | None: @@ -253,9 +230,11 @@ class DemoYoutubePlayer(AbstractDemoPlayer): return self._progress_updated_at return None - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" - self.youtube_id = media_id + self._attr_media_content_id = media_id self.schedule_update_ha_state() def media_pause(self) -> None: @@ -270,7 +249,14 @@ class DemoMusicPlayer(AbstractDemoPlayer): # We only implement the methods that we support + _attr_media_album_name = "Bounzz" + _attr_media_content_id = "bounzz-1" _attr_media_content_type = MediaType.MUSIC + _attr_media_duration = 213 + _attr_media_image_url = ( + "https://graph.facebook.com/v2.5/107771475912710/picture?type=large" + ) + _attr_supported_features = MUSIC_PLAYER_SUPPORT tracks = [ ("Technohead", "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)"), @@ -299,28 +285,8 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Initialize the demo device.""" super().__init__(name) self._cur_track = 0 - self._group_members: list[str] = [] - self._repeat = RepeatMode.OFF - - @property - def group_members(self) -> list[str]: - """List of players which are currently grouped together.""" - return self._group_members - - @property - def media_content_id(self) -> str: - """Return the content ID of current playing media.""" - return "bounzz-1" - - @property - def media_duration(self) -> int: - """Return the duration of current playing media in seconds.""" - return 213 - - @property - def media_image_url(self) -> str: - """Return the image url of current playing media.""" - return "https://graph.facebook.com/v2.5/107771475912710/picture?type=large" + self._attr_group_members: list[str] = [] + self._attr_repeat = RepeatMode.OFF @property def media_title(self) -> str: @@ -332,26 +298,11 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Return the artist of current playing media (Music track only).""" return self.tracks[self._cur_track][0] if self.tracks else "" - @property - def media_album_name(self) -> str: - """Return the album of current playing media (Music track only).""" - return "Bounzz" - @property def media_track(self) -> int: """Return the track number of current media (Music track only).""" return self._cur_track + 1 - @property - def repeat(self) -> RepeatMode: - """Return current repeat mode.""" - return self._repeat - - @property - def supported_features(self) -> int: - """Flag media player features that are supported.""" - return MUSIC_PLAYER_SUPPORT - def media_previous_track(self) -> None: """Send previous track command.""" if self._cur_track > 0: @@ -373,92 +324,56 @@ class DemoMusicPlayer(AbstractDemoPlayer): def set_repeat(self, repeat: RepeatMode) -> None: """Enable/disable repeat mode.""" - self._repeat = repeat + self._attr_repeat = repeat self.schedule_update_ha_state() def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" - self._group_members = [ + self._attr_group_members = [ self.entity_id, ] + group_members self.schedule_update_ha_state() def unjoin_player(self) -> None: """Remove this player from any group.""" - self._group_members = [] + self._attr_group_members = [] self.schedule_update_ha_state() class DemoTVShowPlayer(AbstractDemoPlayer): - """A Demo media player that only supports YouTube.""" + """A Demo media player that only supports Netflix.""" # We only implement the methods that we support + _attr_app_name = "Netflix" + _attr_media_content_id = "house-of-cards-1" _attr_media_content_type = MediaType.TVSHOW + _attr_media_duration = 3600 + _attr_media_image_url = ( + "https://graph.facebook.com/v2.5/HouseofCards/picture?width=400" + ) + _attr_media_season = "1" + _attr_media_series_title = "House of Cards" + _attr_source_list = ["dvd", "youtube"] + _attr_supported_features = NETFLIX_PLAYER_SUPPORT def __init__(self) -> None: """Initialize the demo device.""" super().__init__("Lounge room", MediaPlayerDeviceClass.TV) self._cur_episode = 1 self._episode_count = 13 - self._source = "dvd" - self._source_list = ["dvd", "youtube"] - - @property - def media_content_id(self) -> str: - """Return the content ID of current playing media.""" - return "house-of-cards-1" - - @property - def media_duration(self) -> int: - """Return the duration of current playing media in seconds.""" - return 3600 - - @property - def media_image_url(self) -> str: - """Return the image url of current playing media.""" - return "https://graph.facebook.com/v2.5/HouseofCards/picture?width=400" + self._attr_source = "dvd" @property def media_title(self) -> str: """Return the title of current playing media.""" return f"Chapter {self._cur_episode}" - @property - def media_series_title(self) -> str: - """Return the series title of current playing media (TV Show only).""" - return "House of Cards" - - @property - def media_season(self) -> str: - """Return the season of current playing media (TV Show only).""" - return "1" - @property def media_episode(self) -> str: """Return the episode of current playing media (TV Show only).""" return str(self._cur_episode) - @property - def app_name(self) -> str: - """Return the current running application.""" - return "Netflix" - - @property - def source(self) -> str: - """Return the current input source.""" - return self._source - - @property - def source_list(self) -> list[str]: - """List of available sources.""" - return self._source_list - - @property - def supported_features(self) -> int: - """Flag media player features that are supported.""" - return NETFLIX_PLAYER_SUPPORT - def media_previous_track(self) -> None: """Send previous track command.""" if self._cur_episode > 1: @@ -473,5 +388,5 @@ class DemoTVShowPlayer(AbstractDemoPlayer): def select_source(self, source: str) -> None: """Set the input source.""" - self._source = source + self._attr_source = source self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py index cddc937a71a..1ea00374457 100644 --- a/homeassistant/components/demo/repairs.py +++ b/homeassistant/components/demo/repairs.py @@ -29,6 +29,16 @@ class DemoFixFlow(RepairsFlow): return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) +class DemoColdTeaFixFlow(RepairsFlow): + """Handler for cold tea.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return self.async_abort(reason="not_tea_time") + + async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, @@ -39,5 +49,9 @@ async def async_create_fix_flow( # The bad_psu issue doesn't have its own flow return ConfirmRepairFlow() + if issue_id == "cold_tea": + # The cold_tea issue have it's own flow + return DemoColdTeaFixFlow() + # Other issues have a custom flow return DemoFixFlow() diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index e02d64f157f..7be1a133a74 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -23,6 +23,15 @@ } } }, + "cold_tea": { + "title": "The tea is cold", + "fix_flow": { + "step": {}, + "abort": { + "not_tea_time": "Can not re-heat the tea at this time" + } + } + }, "transmogrifier_deprecated": { "title": "The transmogrifier component is deprecated", "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API" diff --git a/homeassistant/components/demo/text.py b/homeassistant/components/demo/text.py new file mode 100644 index 00000000000..efce1af5c37 --- /dev/null +++ b/homeassistant/components/demo/text.py @@ -0,0 +1,101 @@ +"""Demo platform that offers a fake text entity.""" +from __future__ import annotations + +from homeassistant.components.text import TextEntity, TextMode +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import DOMAIN + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the demo Text entity.""" + async_add_entities( + [ + DemoText( + unique_id="text", + name="Text", + icon=None, + native_value="Hello world", + ), + DemoText( + unique_id="password", + name="Password", + icon="mdi:text", + native_value="Hello world", + mode=TextMode.PASSWORD, + ), + DemoText( + unique_id="text_1_to_5_char", + name="Text with 1 to 5 characters", + icon="mdi:text", + native_value="Hello", + native_min=1, + native_max=5, + ), + DemoText( + unique_id="text_lowercase", + name="Text with only lower case characters", + icon="mdi:text", + native_value="world", + pattern=r"[a-z]+", + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoText(TextEntity): + """Representation of a demo text entity.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + icon: str | None, + native_value: str | None, + mode: TextMode = TextMode.TEXT, + native_max: int | None = None, + native_min: int | None = None, + pattern: str | None = None, + ) -> None: + """Initialize the Demo text entity.""" + self._attr_unique_id = unique_id + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_native_value = native_value + self._attr_icon = icon + self._attr_mode = mode + if native_max is not None: + self._attr_native_max = native_max + if native_min is not None: + self._attr_native_min = native_min + if pattern is not None: + self._attr_pattern = pattern + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) + + async def async_set_value(self, value: str) -> None: + """Update the value.""" + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/demo/translations/bg.json b/homeassistant/components/demo/translations/bg.json index bd761c705ff..98f28e6d881 100644 --- a/homeassistant/components/demo/translations/bg.json +++ b/homeassistant/components/demo/translations/bg.json @@ -11,6 +11,9 @@ }, "title": "\u0417\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0435 \u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u043d\u043e" }, + "cold_tea": { + "title": "\u0427\u0430\u044f\u0442 \u0435 \u0441\u0442\u0443\u0434\u0435\u043d" + }, "unfixable_problem": { "title": "\u0422\u043e\u0432\u0430 \u043d\u0435 \u0435 \u043f\u043e\u043f\u0440\u0430\u0432\u0438\u043c \u043f\u0440\u043e\u0431\u043b\u0435\u043c" } diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index cf6055bfda4..5a126471bf4 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -11,6 +11,14 @@ }, "title": "La font d'alimentaci\u00f3 no \u00e9s estable" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "No es pot tornar a escalfar el te en aquest moment" + } + }, + "title": "El te \u00e9s fred" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/cs.json b/homeassistant/components/demo/translations/cs.json index 1bac6710266..2bc9b387da0 100644 --- a/homeassistant/components/demo/translations/cs.json +++ b/homeassistant/components/demo/translations/cs.json @@ -1,4 +1,14 @@ { + "issues": { + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "V tuto chv\u00edli nelze \u010daj znovu oh\u0159\u00e1t" + } + }, + "title": "\u010caj je studen\u00fd" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index 8f5950f49f8..0318ba7e0cf 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -11,6 +11,14 @@ }, "title": "Das Netzteil ist nicht stabil" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Der Tee kann zu diesem Zeitpunkt nicht wieder aufgew\u00e4rmt werden." + } + }, + "title": "Der Tee ist kalt" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { @@ -23,8 +31,8 @@ "title": "Die Blinkerfl\u00fcssigkeit ist leer und muss nachgef\u00fcllt werden" }, "transmogrifier_deprecated": { - "description": "Die Transmogrifier-Komponente ist jetzt veraltet, da die neue API keine lokale Kontrolle mehr bietet.", - "title": "Die Transmogrifier-Komponente ist veraltet" + "description": "Die Transmogrifier Komponente ist jetzt veraltet, da die neue API keine lokale Kontrolle mehr bietet.", + "title": "Die Transmogrifier Komponente ist veraltet" }, "unfixable_problem": { "description": "Dieses Problem wird niemals aufgeben.", diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index ea1787ab8f0..cb47f52ed42 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -11,6 +11,14 @@ }, "title": "\u03a4\u03bf \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03cc" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b8\u03b5\u03c1\u03bc\u03ac\u03bd\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c4\u03c3\u03ac\u03b9 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae" + } + }, + "title": "\u03a4\u03bf \u03c4\u03c3\u03ac\u03b9 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03cd\u03bf" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index a98f0d3c28d..9f32e982947 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -11,6 +11,14 @@ }, "title": "The power supply is not stable" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Can not re-heat the tea at this time" + } + }, + "title": "The tea is cold" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/es.json b/homeassistant/components/demo/translations/es.json index 3fd439a1b05..70fc94480a7 100644 --- a/homeassistant/components/demo/translations/es.json +++ b/homeassistant/components/demo/translations/es.json @@ -11,6 +11,14 @@ }, "title": "La fuente de alimentaci\u00f3n no es estable." }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "No se puede volver a calentar el t\u00e9 en este momento" + } + }, + "title": "El t\u00e9 est\u00e1 fr\u00edo" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/et.json b/homeassistant/components/demo/translations/et.json index ad7d7e361b6..9cf984ccdd8 100644 --- a/homeassistant/components/demo/translations/et.json +++ b/homeassistant/components/demo/translations/et.json @@ -11,6 +11,14 @@ }, "title": "Toiteallikas ei ole stabiilne" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Teed ei saa praegu uuesti soojendada." + } + }, + "title": "Tee on k\u00fclm" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index 4fa207692f3..754400b5bed 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -10,6 +10,14 @@ }, "title": "L'alimentation \u00e9lectrique n'est pas stable" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Impossible de r\u00e9chauffer le th\u00e9 pour le moment" + } + }, + "title": "Le th\u00e9 est froid" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json index 7acf127caa8..08594a7353b 100644 --- a/homeassistant/components/demo/translations/id.json +++ b/homeassistant/components/demo/translations/id.json @@ -11,6 +11,14 @@ }, "title": "Catu daya tidak stabil" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Tidak bisa memanaskan kembali teh saat ini" + } + }, + "title": "Tehnya dingin" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index e8265baebf7..df45eb12982 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -11,6 +11,14 @@ }, "title": "L'alimentazione non \u00e8 stabile" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Non \u00e8 possibile riscaldare nuovamente il t\u00e8 in questo momento" + } + }, + "title": "Il t\u00e8 \u00e8 freddo" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index 08aeec0fcd0..c1a0cf5590b 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -9,6 +9,14 @@ } }, "title": "De voeding is niet stabiel" + }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Kan op dit moment de thee niet opnieuw verwarmen" + } + }, + "title": "De thee is koud" } }, "options": { diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 396c3a366f6..680ddc11288 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -11,6 +11,14 @@ }, "title": "Str\u00f8mforsyningen er ikke stabil" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "Kan ikke varme opp teen p\u00e5 nytt n\u00e5" + } + }, + "title": "Teen er kald" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index e38a9a1c7aa..16e4bb1396d 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -11,6 +11,14 @@ }, "title": "A fonte de alimenta\u00e7\u00e3o n\u00e3o \u00e9 est\u00e1vel" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "N\u00e3o \u00e9 poss\u00edvel reaquecer o ch\u00e1 neste momento" + } + }, + "title": "O ch\u00e1 est\u00e1 frio" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index b7f3ddb20a7..7f2e10564f8 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -11,6 +11,14 @@ }, "title": "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0438\u0442\u0430\u043d\u0438\u044f \u043d\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u0435\u043d" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u043e\u0434\u043e\u0433\u0440\u0435\u0442\u044c \u0447\u0430\u0439 \u0432 \u044d\u0442\u043e \u0432\u0440\u0435\u043c\u044f." + } + }, + "title": "\u0427\u0430\u0439 \u043e\u0441\u0442\u044b\u043b" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/sk.json b/homeassistant/components/demo/translations/sk.json new file mode 100644 index 00000000000..85f8ceb1ea0 --- /dev/null +++ b/homeassistant/components/demo/translations/sk.json @@ -0,0 +1,37 @@ +{ + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "title": "Nap\u00e1jac\u00ed zdroj je potrebn\u00e9 vymeni\u0165" + } + } + }, + "title": "Nap\u00e1janie nie je stabiln\u00e9" + }, + "cold_tea": { + "title": "\u010caj je studen\u00fd" + }, + "unfixable_problem": { + "title": "Tento probl\u00e9m sa ned\u00e1 odstr\u00e1ni\u0165" + } + }, + "options": { + "step": { + "options_1": { + "data": { + "constant": "Kon\u0161tanta", + "int": "\u010c\u00edseln\u00fd vstup" + } + }, + "options_2": { + "data": { + "multi": "Viacn\u00e1sobn\u00fd v\u00fdber", + "select": "Vyberte mo\u017enos\u0165", + "string": "Hodnota re\u0165azca" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/zh-Hans.json b/homeassistant/components/demo/translations/zh-Hans.json index 1c40afabb6e..69c695d6f6d 100644 --- a/homeassistant/components/demo/translations/zh-Hans.json +++ b/homeassistant/components/demo/translations/zh-Hans.json @@ -1,4 +1,14 @@ { + "issues": { + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "\u76ee\u524d\u65e0\u6cd5\u91cd\u65b0\u52a0\u70ed\u8336" + } + }, + "title": "\u8336\u51c9\u4e86" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/zh-Hant.json b/homeassistant/components/demo/translations/zh-Hant.json index 60f465316a0..0018f67e065 100644 --- a/homeassistant/components/demo/translations/zh-Hant.json +++ b/homeassistant/components/demo/translations/zh-Hant.json @@ -11,6 +11,14 @@ }, "title": "\u96fb\u6e90\u4f9b\u61c9\u4e0d\u7a69\u5b9a" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "\u76ee\u524d\u7121\u6cd5\u518d\u52a0\u71b1\u8336" + } + }, + "title": "\u8336\u6dbc\u4e86" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 015e6b8ca6f..c283ab5456f 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -95,7 +95,7 @@ async def async_setup_platform( DemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES), DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES), DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES), - DemoVacuum(DEMO_VACUUM_NONE, 0), + DemoVacuum(DEMO_VACUUM_NONE, VacuumEntityFeature(0)), StateDemoVacuum(DEMO_VACUUM_STATE), ] ) @@ -106,10 +106,10 @@ class DemoVacuum(VacuumEntity): _attr_should_poll = False - def __init__(self, name: str, supported_features: int) -> None: + def __init__(self, name: str, supported_features: VacuumEntityFeature) -> None: """Initialize the vacuum.""" self._attr_name = name - self._supported_features = supported_features + self._attr_supported_features = supported_features self._state = False self._status = "Charging" self._fan_speed = FAN_SPEEDS[1] @@ -146,11 +146,6 @@ class DemoVacuum(VacuumEntity): """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - def turn_on(self, **kwargs: Any) -> None: """Turn the vacuum on.""" if self.supported_features & VacuumEntityFeature.TURN_ON == 0: @@ -251,21 +246,16 @@ class StateDemoVacuum(StateVacuumEntity): """Representation of a demo vacuum supporting states.""" _attr_should_poll = False + _attr_supported_features = SUPPORT_STATE_SERVICES def __init__(self, name: str) -> None: """Initialize the vacuum.""" self._attr_name = name - self._supported_features = SUPPORT_STATE_SERVICES self._state = STATE_DOCKED self._fan_speed = FAN_SPEEDS[1] self._cleaned_area: float = 0 self._battery_level = 100 - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - @property def state(self) -> str: """Return the current state of the vacuum.""" diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index 322abb0038b..1fc164d5046 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -61,18 +61,11 @@ class DemoWaterHeater(WaterHeaterEntity): """Initialize the water_heater device.""" self._attr_name = name if target_temperature is not None: - self._attr_supported_features = ( - self._attr_supported_features - | WaterHeaterEntityFeature.TARGET_TEMPERATURE - ) + self._attr_supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE if away is not None: - self._attr_supported_features = ( - self._attr_supported_features | WaterHeaterEntityFeature.AWAY_MODE - ) + self._attr_supported_features |= WaterHeaterEntityFeature.AWAY_MODE if current_operation is not None: - self._attr_supported_features = ( - self._attr_supported_features | WaterHeaterEntityFeature.OPERATION_MODE - ) + self._attr_supported_features |= WaterHeaterEntityFeature.OPERATION_MODE self._attr_target_temperature = target_temperature self._attr_temperature_unit = unit_of_measurement self._attr_is_away_mode_on = away diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index cd1a3a6258c..e64d0bcc28d 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -22,14 +22,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - PRESSURE_HPA, - PRESSURE_INHG, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -78,9 +71,9 @@ def setup_platform( 92, 1099, 0.5, - TEMP_CELSIUS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, + UnitOfTemperature.CELSIUS, + UnitOfPressure.HPA, + UnitOfSpeed.METERS_PER_SECOND, [ [ATTR_CONDITION_RAINY, 1, 22, 15, 60], [ATTR_CONDITION_RAINY, 5, 19, 8, 30], @@ -98,9 +91,9 @@ def setup_platform( 54, 987, 4.8, - TEMP_FAHRENHEIT, - PRESSURE_INHG, - SPEED_MILES_PER_HOUR, + UnitOfTemperature.FAHRENHEIT, + UnitOfPressure.INHG, + UnitOfSpeed.MILES_PER_HOUR, [ [ATTR_CONDITION_SNOWY, 2, -10, -15, 60], [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25], diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 8c71dd46c3e..c1f864c8c2f 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -245,7 +245,7 @@ class DenonDevice(MediaPlayerEntity): return self._mediainfo @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self._mediasource in MEDIA_MODES.values(): return SUPPORT_DENON | SUPPORT_MEDIA_MODES diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index cc0e0c06656..aa1fb7361e2 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -8,7 +8,7 @@ import logging from typing import Any, TypeVar from denonavr import DenonAVR -from denonavr.const import POWER_ON +from denonavr.const import POWER_ON, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING from denonavr.exceptions import ( AvrCommandError, AvrForbiddenError, @@ -78,6 +78,14 @@ _R = TypeVar("_R") _P = ParamSpec("_P") +DENON_STATE_MAPPING = { + STATE_ON: MediaPlayerState.ON, + STATE_OFF: MediaPlayerState.OFF, + STATE_PLAYING: MediaPlayerState.PLAYING, + STATE_PAUSED: MediaPlayerState.PAUSED, +} + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -243,9 +251,9 @@ class DenonDevice(MediaPlayerEntity): await self._receiver.async_update_audyssey() @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" - return self._receiver.state + return DENON_STATE_MAPPING.get(self._receiver.state) @property def source_list(self): @@ -277,7 +285,7 @@ class DenonDevice(MediaPlayerEntity): return self._receiver.sound_mode @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self._receiver.input_func in self._receiver.netaudio_func_list: return self._supported_features_base | SUPPORT_MEDIA_MODES diff --git a/homeassistant/components/denonavr/translations/sk.json b/homeassistant/components/denonavr/translations/sk.json index bee0999420f..0957cca2860 100644 --- a/homeassistant/components/denonavr/translations/sk.json +++ b/homeassistant/components/denonavr/translations/sk.json @@ -1,7 +1,34 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_host": "IP adresa prij\u00edma\u010da" + } + }, + "user": { + "data": { + "host": "IP adresa" + }, + "data_description": { + "host": "Ak chcete pou\u017ei\u0165 automatick\u00e9 zis\u0165ovanie, nechajte pole pr\u00e1zdne" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Zobrazi\u0165 v\u0161etky zdroje" + }, + "description": "Zadajte volite\u013en\u00e9 nastavenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index eea2b303a12..b250a910032 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -6,6 +6,7 @@ from typing import Any, cast import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_NAME, CONF_SOURCE, @@ -18,7 +19,6 @@ from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaConfigFlowHandler, SchemaFlowFormStep, - SchemaFlowMenuStep, ) from .const import ( @@ -71,17 +71,17 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): selector.TextSelector(), vol.Required(CONF_SOURCE): selector.EntitySelector( - selector.EntitySelectorConfig(domain="sensor"), + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN), ), } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "user": SchemaFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA), } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 59e661fce0b..8b8bc2f59f7 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta from decimal import Decimal, DecimalException import logging +from typing import TYPE_CHECKING import voluptuous as vol @@ -137,20 +138,20 @@ class DerivativeSensor(RestoreEntity, SensorEntity): def __init__( self, *, - name, - round_digits, - source_entity, - time_window, - unit_of_measurement, - unit_prefix, - unit_time, - unique_id, - ): + name: str | None, + round_digits: int, + source_entity: str, + time_window: timedelta, + unit_of_measurement: str | None, + unit_prefix: str | None, + unit_time: str, + unique_id: str | None, + ) -> None: """Initialize the derivative sensor.""" self._attr_unique_id = unique_id self._sensor_source_id = source_entity self._round_digits = round_digits - self._state = 0 + self._state: float | int | Decimal = 0 # List of tuples with (timestamp_start, timestamp_end, derivative) self._state_list: list[tuple[datetime, datetime, Decimal]] = [] @@ -231,7 +232,9 @@ class DerivativeSensor(RestoreEntity, SensorEntity): (old_state.last_updated, new_state.last_updated, new_derivative) ) - def calculate_weight(start, end, now): + def calculate_weight( + start: datetime, end: datetime, now: datetime + ) -> float: window_start = now - timedelta(seconds=self._time_window) if start < window_start: weight = (end - window_start).total_seconds() / self._time_window @@ -259,6 +262,9 @@ class DerivativeSensor(RestoreEntity, SensorEntity): ) @property - def native_value(self): + def native_value(self) -> float | int | Decimal: """Return the state of the sensor.""" - return round(self._state, self._round_digits) + value = round(self._state, self._round_digits) + if TYPE_CHECKING: + assert isinstance(value, (float, int, Decimal)) + return value diff --git a/homeassistant/components/derivative/translations/sk.json b/homeassistant/components/derivative/translations/sk.json index cc4a1cc7cb9..bd461eb44e8 100644 --- a/homeassistant/components/derivative/translations/sk.json +++ b/homeassistant/components/derivative/translations/sk.json @@ -3,7 +3,27 @@ "step": { "user": { "data": { - "name": "Meno" + "name": "Meno", + "round": "Presnos\u0165", + "source": "Vstupn\u00fd sn\u00edma\u010d", + "time_window": "\u010casov\u00e9 okno", + "unit_time": "\u010casov\u00e1 jednotka" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Meno", + "round": "Presnos\u0165", + "source": "Vstupn\u00fd sn\u00edma\u010d", + "time_window": "\u010casov\u00e9 okno", + "unit_time": "\u010casov\u00e1 jednotka" + }, + "data_description": { + "unit_prefix": "." } } } diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index bd72b24d844..05f2f79ff28 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -5,8 +5,9 @@ from typing import Any, Protocol, cast import voluptuous as vol -from homeassistant.const import CONF_DOMAIN +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -63,6 +64,27 @@ async def async_validate_trigger_config( ) if not hasattr(platform, "async_validate_trigger_config"): return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) + + # Only call the dynamic validator if the relevant config entry is loaded + registry = dr.async_get(hass) + if not (device := registry.async_get(config[CONF_DEVICE_ID])): + raise InvalidDeviceAutomationConfig + + device_config_entry = None + for entry_id in device.config_entries: + if not (entry := hass.config_entries.async_get_entry(entry_id)): + continue + if entry.domain != config[CONF_DOMAIN]: + continue + device_config_entry = entry + break + + if not device_config_entry: + raise InvalidDeviceAutomationConfig + + if not await hass.config_entries.async_wait_component(device_config_entry): + return config + return await platform.async_validate_trigger_config(hass, config) except InvalidDeviceAutomationConfig as err: raise vol.Invalid(str(err) or "Invalid trigger configuration") from err diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 2d3353a1110..a6a8e9d2d8c 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -6,7 +6,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from .config_entry import async_setup_entry, async_unload_entry # noqa: F401 +from .config_entry import ( # noqa: F401 + ScannerEntity, + TrackerEntity, + async_setup_entry, + async_unload_entry, +) from .const import ( # noqa: F401 ATTR_ATTRIBUTES, ATTR_BATTERY, diff --git a/homeassistant/components/device_tracker/translations/de.json b/homeassistant/components/device_tracker/translations/de.json index fe59183e67a..06ea2406bd5 100644 --- a/homeassistant/components/device_tracker/translations/de.json +++ b/homeassistant/components/device_tracker/translations/de.json @@ -1,8 +1,8 @@ { "device_automation": { "condition_type": { - "is_home": "{entity_name} ist zuhause", - "is_not_home": "{entity_name} ist nicht zuhause" + "is_home": "{entity_name} ist zu Hause", + "is_not_home": "{entity_name} ist nicht zu Hause" }, "trigger_type": { "enters": "{entity_name} betritt einen Bereich", diff --git a/homeassistant/components/device_tracker/translations/is.json b/homeassistant/components/device_tracker/translations/is.json index 433d2a6afb8..3696d2a25a4 100644 --- a/homeassistant/components/device_tracker/translations/is.json +++ b/homeassistant/components/device_tracker/translations/is.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_home": "{entity_name} er heima", + "is_not_home": "{entity_name} er ekki heima" + } + }, "state": { "_": { "home": "Heima", diff --git a/homeassistant/components/device_tracker/translations/sk.json b/homeassistant/components/device_tracker/translations/sk.json index 9d52c35e2cb..d77c8c7d915 100644 --- a/homeassistant/components/device_tracker/translations/sk.json +++ b/homeassistant/components/device_tracker/translations/sk.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_home": "{entity_name} je doma", + "is_not_home": "{entity_name} nie je doma" + }, + "trigger_type": { + "enters": "{entity_name} vst\u00fapi do z\u00f3ny", + "leaves": "{entity_name} opust\u00ed z\u00f3nu" + } + }, "state": { "_": { "home": "Doma", diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index a6b18c2b312..e8395062e8c 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -81,13 +81,14 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): or self._binary_sensor_property.sensor_type ) - if self._attr_device_class is None: - if device_instance.binary_sensor_property[element_uid].sub_type != "": - self._attr_name += ( - f" {device_instance.binary_sensor_property[element_uid].sub_type}" - ) - else: - self._attr_name += f" {device_instance.binary_sensor_property[element_uid].sensor_type}" + if device_instance.binary_sensor_property[element_uid].sub_type != "": + self._attr_name = device_instance.binary_sensor_property[ + element_uid + ].sub_type.capitalize() + else: + self._attr_name = device_instance.binary_sensor_property[ + element_uid + ].sensor_type.capitalize() self._value = self._binary_sensor_property.state @@ -128,6 +129,7 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): self._key = key self._attr_is_on = False + self._attr_name = f"Button {key}" def _sync(self, message: tuple) -> None: """Update the binary sensor state.""" diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 6c566aa45e3..227b4796883 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -8,13 +8,12 @@ from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.climate import ( ATTR_TEMPERATURE, - TEMP_CELSIUS, ClimateEntity, ClimateEntityFeature, HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -68,7 +67,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit self._attr_precision = PRECISION_TENTHS self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE self._attr_target_temperature_step = PRECISION_HALVES - self._attr_temperature_unit = TEMP_CELSIUS + self._attr_temperature_unit = UnitOfTemperature.CELSIUS @property def current_temperature(self) -> float | None: diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 6087e07799d..5848f682626 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -19,6 +19,8 @@ _LOGGER = logging.getLogger(__name__) class DevoloDeviceEntity(Entity): """Abstract representation of a device within devolo Home Control.""" + _attr_has_entity_name = True + def __init__( self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str ) -> None: @@ -29,9 +31,6 @@ class DevoloDeviceEntity(Entity): self._attr_available = ( device_instance.is_online() ) # This is not doing I/O. It fetches an internal state of the API - self._attr_name: str = device_instance.settings_property[ - "general_device_settings" - ].name self._attr_should_poll = False self._attr_unique_id = element_uid self._attr_device_info = DeviceInfo( @@ -39,7 +38,7 @@ class DevoloDeviceEntity(Entity): identifiers={(DOMAIN, self._device_instance.uid)}, manufacturer=device_instance.brand, model=device_instance.name, - name=self._attr_name, + name=device_instance.settings_property["general_device_settings"].name, suggested_area=device_instance.settings_property[ "general_device_settings" ].zone, @@ -47,11 +46,16 @@ class DevoloDeviceEntity(Entity): self.subscriber: Subscriber | None = None self.sync_callback = self._sync + self._value: float async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" - self.subscriber = Subscriber(self._attr_name, callback=self.sync_callback) + assert self.device_info + assert self.device_info["name"] # The name was set on entity creation + self.subscriber = Subscriber( + self.device_info["name"], callback=self.sync_callback + ) self._homecontrol.publisher.register( self._device_instance.uid, self.subscriber, self.sync_callback ) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 2d023d23e2d..33a17929f71 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -115,12 +115,9 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): self._multi_level_sensor_property.sensor_type ) self._attr_native_unit_of_measurement = self._multi_level_sensor_property.unit - + self._attr_name = self._multi_level_sensor_property.sensor_type.capitalize() self._value = self._multi_level_sensor_property.value - if self._attr_device_class is None: - self._attr_name += f" {self._multi_level_sensor_property.sensor_type}" - if element_uid.startswith("devolo.VoltageMultiLevelSensor:"): self._attr_entity_registry_enabled_default = False @@ -143,7 +140,7 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): self._attr_state_class = STATE_CLASS_MAPPING.get("battery") self._attr_entity_category = EntityCategory.DIAGNOSTIC self._attr_native_unit_of_measurement = PERCENTAGE - + self._attr_name = "Battery level" self._value = device_instance.battery_level @@ -179,7 +176,7 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): device_instance.consumption_property[element_uid], consumption ) - self._attr_name += f" {consumption}" + self._attr_name = f"{consumption.capitalize()} consumption" @property def unique_id(self) -> str: diff --git a/homeassistant/components/devolo_home_control/translations/bg.json b/homeassistant/components/devolo_home_control/translations/bg.json index 47ab5f03cbc..3741bfd4438 100644 --- a/homeassistant/components/devolo_home_control/translations/bg.json +++ b/homeassistant/components/devolo_home_control/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/sk.json b/homeassistant/components/devolo_home_control/translations/sk.json index 9273954369f..94a5289e28f 100644 --- a/homeassistant/components/devolo_home_control/translations/sk.json +++ b/homeassistant/components/devolo_home_control/translations/sk.json @@ -1,19 +1,25 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "reauth_failed": "Pou\u017eite rovnak\u00e9ho pou\u017e\u00edvate\u013ea mydevolo ako predt\u00fdm." }, "step": { "user": { "data": { + "mydevolo_url": "mydevolo URL", + "password": "Heslo", "username": "Email / devolo ID" } }, "zeroconf_confirm": { "data": { + "mydevolo_url": "mydevolo URL", + "password": "Heslo", "username": "Email / devolo ID" } } diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index f535136680a..26b30ec43d5 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -7,9 +7,9 @@ from devolo_plc_api.device import Device from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, + ScannerEntity, SourceType, ) -from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import FREQUENCY_GIGAHERTZ, STATE_UNKNOWN from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/devolo_home_network/translations/bg.json b/homeassistant/components/devolo_home_network/translations/bg.json index a90c099889a..44d409938a0 100644 --- a/homeassistant/components/devolo_home_network/translations/bg.json +++ b/homeassistant/components/devolo_home_network/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/devolo_home_network/translations/cs.json b/homeassistant/components/devolo_home_network/translations/cs.json index 04f18366eaf..42631f030f3 100644 --- a/homeassistant/components/devolo_home_network/translations/cs.json +++ b/homeassistant/components/devolo_home_network/translations/cs.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "ip_address": "IP adresa" diff --git a/homeassistant/components/devolo_home_network/translations/el.json b/homeassistant/components/devolo_home_network/translations/el.json index 45a54b59b00..e78c38ea13e 100644 --- a/homeassistant/components/devolo_home_network/translations/el.json +++ b/homeassistant/components/devolo_home_network/translations/el.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "home_control": "\u0397 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ae \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 Home \u03c4\u03b7\u03c2 devolo \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." + "home_control": "\u0397 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ae \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 Home \u03c4\u03b7\u03c2 devolo \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + }, "user": { "data": { "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" diff --git a/homeassistant/components/devolo_home_network/translations/hr.json b/homeassistant/components/devolo_home_network/translations/hr.json new file mode 100644 index 00000000000..3e7836e5961 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/hr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponovna provjera autenti\u010dnosti je uspje\u0161na" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Lozinka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/sk.json b/homeassistant/components/devolo_home_network/translations/sk.json new file mode 100644 index 00000000000..45116ef4db3 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/sk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "home_control": "Centr\u00e1lna jednotka devolo Home Control nefunguje s touto integr\u00e1ciou.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{product} ({name})", + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, + "user": { + "data": { + "ip_address": "IP adresa" + }, + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + }, + "zeroconf_confirm": { + "title": "Objaven\u00e9 zariadenie dom\u00e1cej siete devolo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/sk.json b/homeassistant/components/dexcom/translations/sk.json index 5ada995aa6e..607228fd38e 100644 --- a/homeassistant/components/dexcom/translations/sk.json +++ b/homeassistant/components/dexcom/translations/sk.json @@ -1,7 +1,31 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "server": "Server", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Nastavte integr\u00e1ciu Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Jednotka merania" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/diagnostics/translations/sk.json b/homeassistant/components/diagnostics/translations/sk.json new file mode 100644 index 00000000000..eb0d3fdde9e --- /dev/null +++ b/homeassistant/components/diagnostics/translations/sk.json @@ -0,0 +1,3 @@ +{ + "title": "Diagnostiky" +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/sk.json b/homeassistant/components/dialogflow/translations/sk.json new file mode 100644 index 00000000000..933f73976d2 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index bc838757854..21b25962fce 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -237,7 +237,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): return self._program.channel @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" return SUPPORT_DTV_CLIENT if self._is_client else SUPPORT_DTV diff --git a/homeassistant/components/directv/translations/bg.json b/homeassistant/components/directv/translations/bg.json index b43da9ecb18..371990a6d32 100644 --- a/homeassistant/components/directv/translations/bg.json +++ b/homeassistant/components/directv/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/directv/translations/sk.json b/homeassistant/components/directv/translations/sk.json new file mode 100644 index 00000000000..22f56dce8ab --- /dev/null +++ b/homeassistant/components/directv/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "ssdp_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index b631c5fa7e7..022cb5fd933 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -6,5 +6,6 @@ "requirements": ["nextcord==2.0.0a8"], "codeowners": ["@tkdrob"], "iot_class": "cloud_push", - "loggers": ["discord"] + "loggers": ["discord"], + "integration_type": "service" } diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index d97ce7042bc..8fcab7cefba 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -1,6 +1,7 @@ """Discord platform for notify component.""" from __future__ import annotations +from io import BytesIO import logging import os.path from typing import Any, cast @@ -15,6 +16,7 @@ from homeassistant.components.notify import ( ) from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -30,6 +32,10 @@ ATTR_EMBED_THUMBNAIL = "thumbnail" ATTR_EMBED_IMAGE = "image" ATTR_EMBED_URL = "url" ATTR_IMAGES = "images" +ATTR_URLS = "urls" +ATTR_VERIFY_SSL = "verify_ssl" + +MAX_ALLOWED_DOWNLOAD_SIZE_BYTES = 8000000 async def async_get_service( @@ -61,11 +67,54 @@ class DiscordNotificationService(BaseNotificationService): return False return True + async def async_get_file_from_url( + self, url: str, verify_ssl: bool, max_file_size: int + ) -> bytearray | None: + """Retrieve file bytes from URL.""" + if not self.hass.config.is_allowed_external_url(url): + _LOGGER.error("URL not allowed: %s", url) + return None + + session = async_get_clientsession(self.hass) + + async with session.get( + url, + ssl=verify_ssl, + timeout=30, + raise_for_status=True, + ) as resp: + content_length = resp.headers.get("Content-Length") + + if content_length is not None and int(content_length) > max_file_size: + _LOGGER.error( + "Attachment too large (Content-Length reports %s). Max size: %s bytes", + int(content_length), + max_file_size, + ) + return None + + file_size = 0 + byte_chunks = bytearray() + + async for byte_chunk, _ in resp.content.iter_chunks(): + file_size += len(byte_chunk) + if file_size > max_file_size: + _LOGGER.error( + "Attachment too large (Stream reports %s). Max size: %s bytes", + file_size, + max_file_size, + ) + return None + + byte_chunks.extend(byte_chunk) + + return byte_chunks + async def async_send_message(self, message: str, **kwargs: Any) -> None: """Login to Discord, send message to channel(s) and log out.""" nextcord.VoiceClient.warn_nacl = False discord_bot = nextcord.Client() - images = None + images = [] embedding = None if ATTR_TARGET not in kwargs: @@ -100,15 +149,28 @@ class DiscordNotificationService(BaseNotificationService): embeds.append(embed) if ATTR_IMAGES in data: - images = [] - for image in data.get(ATTR_IMAGES, []): image_exists = await self.hass.async_add_executor_job( self.file_exists, image ) + filename = os.path.basename(image) + if image_exists: - images.append(image) + images.append((image, filename)) + + if ATTR_URLS in data: + for url in data.get(ATTR_URLS, []): + file = await self.async_get_file_from_url( + url, + data.get(ATTR_VERIFY_SSL, True), + MAX_ALLOWED_DOWNLOAD_SIZE_BYTES, + ) + + if file is not None: + filename = os.path.basename(url) + + images.append((BytesIO(file), filename)) await discord_bot.login(self.token) @@ -116,7 +178,7 @@ class DiscordNotificationService(BaseNotificationService): for channelid in kwargs[ATTR_TARGET]: channelid = int(channelid) # Must create new instances of File for each channel. - files = [nextcord.File(image) for image in images] if images else [] + files = [nextcord.File(image, filename) for image, filename in images] try: channel = cast( Messageable, await discord_bot.fetch_channel(channelid) diff --git a/homeassistant/components/discord/translations/bg.json b/homeassistant/components/discord/translations/bg.json index 00faba1155f..2d2d1253e4a 100644 --- a/homeassistant/components/discord/translations/bg.json +++ b/homeassistant/components/discord/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/discord/translations/sk.json b/homeassistant/components/discord/translations/sk.json new file mode 100644 index 00000000000..f20eb067fd9 --- /dev/null +++ b/homeassistant/components/discord/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API token" + } + }, + "user": { + "data": { + "api_token": "API token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index e4620386b98..3da3de8434f 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.32.2"], + "requirements": ["async-upnp-client==0.32.3"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index ff09f018639..d2c61e9a318 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -478,15 +478,15 @@ class DlnaDmrEntity(MediaPlayerEntity): return MediaPlayerState.IDLE @property - def supported_features(self) -> int: + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported at this moment. Supported features may change as the device enters different states. """ if not self._device: - return 0 + return MediaPlayerEntityFeature(0) - supported_features = 0 + supported_features = MediaPlayerEntityFeature(0) if self._device.has_volume_level: supported_features |= MediaPlayerEntityFeature.VOLUME_SET diff --git a/homeassistant/components/dlna_dmr/translations/sk.json b/homeassistant/components/dlna_dmr/translations/sk.json new file mode 100644 index 00000000000..186ad1deb41 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/sk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "alternative_integration": "Zariadenie je lep\u0161ie podporovan\u00e9 inou integr\u00e1ciou", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "non_unique_id": "Viacer\u00e9 zariadenia n\u00e1jden\u00e9 s rovnak\u00fdm jedine\u010dn\u00fdm identifik\u00e1torom" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL adresa k XML s\u00faboru popisu zariadenia" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nakonfigurova\u0165, alebo nechajte pr\u00e1zdne a zadajte adresu URL" + } + } + }, + "options": { + "error": { + "invalid_url": "Neplatn\u00e1 adresa URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 7c7a312159b..3630e5ce309 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.32.2"], + "requirements": ["async-upnp-client==0.32.3"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/translations/he.json b/homeassistant/components/dlna_dms/translations/he.json index cfe995b8921..41bb4ddbf8b 100644 --- a/homeassistant/components/dlna_dms/translations/he.json +++ b/homeassistant/components/dlna_dms/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/dlna_dms/translations/it.json b/homeassistant/components/dlna_dms/translations/it.json index a8c0d50c0cf..c671bc9b619 100644 --- a/homeassistant/components/dlna_dms/translations/it.json +++ b/homeassistant/components/dlna_dms/translations/it.json @@ -17,7 +17,7 @@ "host": "Host" }, "description": "Seleziona un dispositivo da configurare", - "title": "Dispositivi DLNA DMA rilevati" + "title": "Rilevati dispositivi DLNA DMA" } } } diff --git a/homeassistant/components/dlna_dms/translations/sk.json b/homeassistant/components/dlna_dms/translations/sk.json new file mode 100644 index 00000000000..537609a4407 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nakonfigurova\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 93bf73f1b9d..bbc15f1b139 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -74,7 +74,7 @@ class WanIpSensor(SensorEntity): } self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"{hostname}_{ipv6}")}, + identifiers={(DOMAIN, hostname)}, manufacturer="DNS", model=aiodns.__version__, name=name, diff --git a/homeassistant/components/dnsip/translations/cs.json b/homeassistant/components/dnsip/translations/cs.json new file mode 100644 index 00000000000..92fbbc7fe04 --- /dev/null +++ b/homeassistant/components/dnsip/translations/cs.json @@ -0,0 +1,7 @@ +{ + "options": { + "error": { + "invalid_resolver": "Neplatn\u00e1 IP adresa pro resolver" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/de.json b/homeassistant/components/dnsip/translations/de.json index 9d82d5c7655..93a4bf8fb26 100644 --- a/homeassistant/components/dnsip/translations/de.json +++ b/homeassistant/components/dnsip/translations/de.json @@ -7,21 +7,21 @@ "user": { "data": { "hostname": "Der Hostname, f\u00fcr den die DNS-Abfrage durchgef\u00fchrt werden soll", - "resolver": "Resolver f\u00fcr IPV4-Lookup", - "resolver_ipv6": "Resolver f\u00fcr IPV6-Lookup" + "resolver": "Aufl\u00f6ser f\u00fcr IPv4-Suche", + "resolver_ipv6": "Aufl\u00f6ser f\u00fcr IPv6-Suche" } } } }, "options": { "error": { - "invalid_resolver": "Ung\u00fcltige IP-Adresse f\u00fcr Resolver" + "invalid_resolver": "Ung\u00fcltige IP-Adresse f\u00fcr Aufl\u00f6ser" }, "step": { "init": { "data": { - "resolver": "Resolver f\u00fcr IPV4-Lookup", - "resolver_ipv6": "Resolver f\u00fcr IPV6-Lookup" + "resolver": "Aufl\u00f6ser f\u00fcr IPv4-Suche", + "resolver_ipv6": "Aufl\u00f6ser f\u00fcr IPv6-Suche" } } } diff --git a/homeassistant/components/dnsip/translations/ru.json b/homeassistant/components/dnsip/translations/ru.json index a4421b566f6..0882153776a 100644 --- a/homeassistant/components/dnsip/translations/ru.json +++ b/homeassistant/components/dnsip/translations/ru.json @@ -1,12 +1,12 @@ { "config": { "error": { - "invalid_hostname": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f." + "invalid_hostname": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430." }, "step": { "user": { "data": { - "hostname": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f DNS-\u0437\u0430\u043f\u0440\u043e\u0441", + "hostname": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f DNS-\u0437\u0430\u043f\u0440\u043e\u0441", "resolver": "\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 IPV4", "resolver_ipv6": "\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 IPV6" } diff --git a/homeassistant/components/dnsip/translations/sk.json b/homeassistant/components/dnsip/translations/sk.json new file mode 100644 index 00000000000..06557ec1e79 --- /dev/null +++ b/homeassistant/components/dnsip/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "invalid_hostname": "Neplatn\u00fd n\u00e1zov hostite\u013ea" + } + }, + "options": { + "error": { + "invalid_resolver": "Neplatn\u00e1 adresa IP pre resolver" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 766167b7af9..39ed9c552bb 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.2.0"], + "requirements": ["pydoods==1.0.2", "pillow==9.3.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index fce76a65ff5..5983e639851 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -96,9 +96,8 @@ class DoorBirdCamera(DoorBirdEntity, Camera): self._stream_url = stream_url self._attr_name = name self._last_image: bytes | None = None - self._attr_supported_features = ( - CameraEntityFeature.STREAM if self._stream_url else 0 - ) + if self._stream_url: + self._attr_supported_features = CameraEntityFeature.STREAM self._interval = interval self._last_update = datetime.datetime.min self._attr_unique_id = f"{self._mac_addr}_{camera_id}" diff --git a/homeassistant/components/doorbird/translations/bg.json b/homeassistant/components/doorbird/translations/bg.json index 628eaf62894..02f7ea25f5f 100644 --- a/homeassistant/components/doorbird/translations/bg.json +++ b/homeassistant/components/doorbird/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/doorbird/translations/sk.json b/homeassistant/components/doorbird/translations/sk.json index 5ada995aa6e..b6e9348c1fc 100644 --- a/homeassistant/components/doorbird/translations/sk.json +++ b/homeassistant/components/doorbird/translations/sk.json @@ -1,7 +1,33 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "link_local_address": "Lok\u00e1lne adresy odkazov nie s\u00fa podporovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "name": "N\u00e1zov zariadenia", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Zoznam udalost\u00ed oddelen\u00fdch \u010diarkou." + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index aa01c798072..5cbd3e14be0 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -30,7 +30,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, VOLUME_CUBIC_METERS, ) -from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import EventType, StateType @@ -505,6 +505,15 @@ async def async_setup_entry( # Can't be hass.async_add_job because job runs forever task = asyncio.create_task(connect_and_reconnect()) + @callback + async def _async_stop(_: Event) -> None: + task.cancel() + + # Make sure task is cancelled on shutdown (or tests complete) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + ) + # Save the task to be able to cancel it when unloading hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task @@ -559,6 +568,7 @@ class DSMREntity(SensorEntity): @property def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" + value: StateType if (value := self.get_dsmr_object_attr("value")) is None: return None @@ -573,10 +583,7 @@ class DSMREntity(SensorEntity): float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION) ) - if value is not None: - return value - - return None + return value @property def native_unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/dsmr/translations/sk.json b/homeassistant/components/dsmr/translations/sk.json index e343d2e8b31..d9e7b05f578 100644 --- a/homeassistant/components/dsmr/translations/sk.json +++ b/homeassistant/components/dsmr/translations/sk.json @@ -1,10 +1,52 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_communicate": "Komunik\u00e1cia zlyhala", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_communicate": "Komunik\u00e1cia zlyhala", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "setup_network": { "data": { + "dsmr_version": "Vyberte verziu DSMR", + "host": "Hostite\u013e", "port": "Port" - } + }, + "title": "Vyberte adresu pripojenia" + }, + "setup_serial": { + "data": { + "dsmr_version": "Vyberte verziu DSMR", + "port": "Vyberte zariadenie" + }, + "title": "Zariadenie" + }, + "setup_serial_manual_path": { + "data": { + "port": "Cesta k zariadeniu USB" + }, + "title": "Cesta" + }, + "user": { + "data": { + "type": "Typ pripojenia" + }, + "title": "Vyberte typ pripojenia" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minim\u00e1lny \u010das medzi aktualiz\u00e1ciami entity [s]" + }, + "title": "Mo\u017enosti DSMR" } } } diff --git a/homeassistant/components/dsmr_reader/translations/cs.json b/homeassistant/components/dsmr_reader/translations/cs.json new file mode 100644 index 00000000000..19f5d1e1587 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/de.json b/homeassistant/components/dsmr_reader/translations/de.json index 963a3a74e2e..05a69559286 100644 --- a/homeassistant/components/dsmr_reader/translations/de.json +++ b/homeassistant/components/dsmr_reader/translations/de.json @@ -12,7 +12,7 @@ "issues": { "deprecated_yaml": { "description": "Die Konfiguration von DSMR Reader mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die DSMR Reader YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die DSMR-Reader-Konfiguration wird entfernt" + "title": "Die DSMR-Reader Konfiguration wird entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/sk.json b/homeassistant/components/dsmr_reader/translations/sk.json new file mode 100644 index 00000000000..c294bc45d7c --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 4770492876d..a184b91c05e 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -19,7 +19,7 @@ from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN CONF_SOURCES: Final = "sources" -DUNEHD_PLAYER_SUPPORT: Final[int] = ( +DUNEHD_PLAYER_SUPPORT: Final[MediaPlayerEntityFeature] = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF @@ -105,7 +105,7 @@ class DuneHDPlayerEntity(MediaPlayerEntity): return int(self._state.get("playback_mute", 0)) == 1 @property - def supported_features(self) -> int: + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" return DUNEHD_PLAYER_SUPPORT diff --git a/homeassistant/components/dunehd/translations/ru.json b/homeassistant/components/dunehd/translations/ru.json index 8c32af72af7..f0d5e989dd2 100644 --- a/homeassistant/components/dunehd/translations/ru.json +++ b/homeassistant/components/dunehd/translations/ru.json @@ -6,7 +6,7 @@ "error": { "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_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "step": { "user": { diff --git a/homeassistant/components/dunehd/translations/sk.json b/homeassistant/components/dunehd/translations/sk.json new file mode 100644 index 00000000000..55314bfd4fa --- /dev/null +++ b/homeassistant/components/dunehd/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index e5c38996a89..bbca16d3db6 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -2,7 +2,12 @@ from typing import Any -from homeassistant.components.cover import DEVICE_CLASSES, CoverDeviceClass, CoverEntity +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + DEVICE_CLASSES, + CoverDeviceClass, + CoverEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,6 +83,12 @@ class DynaliteCover(DynaliteBase, CoverEntity): """Stop the cover.""" await self._device.async_stop_cover(**kwargs) + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = state.attributes.get(ATTR_CURRENT_POSITION) + if target_level is not None: + self._device.init_level(target_level) + class DynaliteCoverWithTilt(DynaliteCover): """Representation of a Dynalite Channel as a Home Assistant Cover that uses up and down for tilt.""" diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index b4b8285cbb0..3ebf04ab219 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -1,14 +1,16 @@ """Support for the Dynalite devices as entities.""" from __future__ import annotations +from abc import ABC, abstractmethod from collections.abc import Callable from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from .bridge import DynaliteBridge from .const import DOMAIN, LOGGER @@ -36,7 +38,7 @@ def async_setup_entry_base( bridge.register_add_devices(platform, async_add_entities_platform) -class DynaliteBase(Entity): +class DynaliteBase(RestoreEntity, ABC): """Base class for the Dynalite entities.""" def __init__(self, device: Any, bridge: DynaliteBridge) -> None: @@ -70,8 +72,16 @@ class DynaliteBase(Entity): ) async def async_added_to_hass(self) -> None: - """Added to hass so need to register to dispatch.""" + """Added to hass so need to restore state and register to dispatch.""" # register for device specific update + await super().async_added_to_hass() + + cur_state = await self.async_get_last_state() + if cur_state: + self.initialize_state(cur_state) + else: + LOGGER.info("Restore state not available for %s", self.entity_id) + self._unsub_dispatchers.append( async_dispatcher_connect( self.hass, @@ -88,6 +98,10 @@ class DynaliteBase(Entity): ) ) + @abstractmethod + def initialize_state(self, state): + """Initialize the state from cache.""" + async def async_will_remove_from_hass(self) -> None: """Unregister signal dispatch listeners when being removed.""" for unsub in self._unsub_dispatchers: diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index 27cd6f8cae8..ffb97da49c1 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -2,7 +2,7 @@ from typing import Any -from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -44,3 +44,9 @@ class DynaliteLight(DynaliteBase, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.async_turn_off(**kwargs) + + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = state.attributes.get(ATTR_BRIGHTNESS) + if target_level is not None: + self._device.init_level(target_level) diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index d403291a081..57010666019 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.46"], + "requirements": ["dynalite_devices==0.1.47"], "iot_class": "local_push", "loggers": ["dynalite_devices_lib"] } diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index 3e459e45847..54e9b919b89 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -4,6 +4,7 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,3 +37,8 @@ class DynaliteSwitch(DynaliteBase, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.async_turn_off() + + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = 1 if state.state == STATE_ON else 0 + self._device.init_level(target_level) diff --git a/homeassistant/components/eafm/translations/sk.json b/homeassistant/components/eafm/translations/sk.json new file mode 100644 index 00000000000..a3d22ff7b90 --- /dev/null +++ b/homeassistant/components/eafm/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "station": "Stanica" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index 776a573faf6..14158b6c379 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -111,7 +111,7 @@ class EbusdData: raise RuntimeError(err) from err def write(self, call: ServiceCall) -> None: - """Call write methon on ebusd.""" + """Call write method on ebusd.""" name = call.data.get("name") value = call.data.get("value") diff --git a/homeassistant/components/ebusd/translations/sk.json b/homeassistant/components/ebusd/translations/sk.json new file mode 100644 index 00000000000..674290ece76 --- /dev/null +++ b/homeassistant/components/ebusd/translations/sk.json @@ -0,0 +1,5 @@ +{ + "state": { + "day": "De\u0148" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 16870d76902..028979b2a85 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -351,7 +351,7 @@ class Thermostat(ClimateEntity): return self.thermostat["runtime"]["connected"] @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" if self.has_humidifier_control: return SUPPORT_FLAGS | ClimateEntityFeature.TARGET_HUMIDITY diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 9d8793efc29..30949e36f8e 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -76,7 +76,6 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( key="airQuality", name="Air Quality Index", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement=None, state_class=SensorStateClass.MEASUREMENT, runtime_key="actualAQScore", ), diff --git a/homeassistant/components/ecobee/translations/de.json b/homeassistant/components/ecobee/translations/de.json index 10edbd4ecd1..d9d138936cb 100644 --- a/homeassistant/components/ecobee/translations/de.json +++ b/homeassistant/components/ecobee/translations/de.json @@ -5,7 +5,7 @@ }, "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." + "token_request_failed": "Fehler beim Anfordern eines Tokens von ecobee; Bitte versuche es erneut." }, "step": { "authorize": { diff --git a/homeassistant/components/ecobee/translations/sk.json b/homeassistant/components/ecobee/translations/sk.json index 9d5ee388dc3..619f56c68f3 100644 --- a/homeassistant/components/ecobee/translations/sk.json +++ b/homeassistant/components/ecobee/translations/sk.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "token_request_failed": "Chyba pri vy\u017eiadan\u00ed tokenov od ecobee; pros\u00edm sk\u00faste znova." + }, "step": { + "authorize": { + "description": "Autorizujte t\u00fato aplik\u00e1ciu na https://www.ecobee.com/consumerportal/index.html pomocou k\u00f3du PIN: \n\n {pin}\n\n Potom stla\u010dte tla\u010didlo Odosla\u0165." + }, "user": { "data": { "api_key": "API k\u013e\u00fa\u010d" - } + }, + "title": "ecobee API k\u013e\u00fa\u010d" } } } diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 69f02d26294..5610cdb2a9c 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -16,10 +16,10 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - LENGTH_METERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_FAHRENHEIT, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -53,10 +53,10 @@ async def async_setup_entry( class EcobeeWeather(WeatherEntity): """Representation of Ecobee weather data.""" - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_FAHRENHEIT - _attr_native_visibility_unit = LENGTH_METERS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.FAHRENHEIT + _attr_native_visibility_unit = UnitOfLength.METERS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index bda462285fc..cf950a3c38c 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -81,7 +81,7 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): self.op_list.append(ha_mode) @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" if self._econet.supports_humidifier: return SUPPORT_FLAGS_THERMOSTAT | ClimateEntityFeature.TARGET_HUMIDITY diff --git a/homeassistant/components/econet/translations/bg.json b/homeassistant/components/econet/translations/bg.json index 637413ad06d..aeec4d24e19 100644 --- a/homeassistant/components/econet/translations/bg.json +++ b/homeassistant/components/econet/translations/bg.json @@ -4,6 +4,9 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/econet/translations/sk.json b/homeassistant/components/econet/translations/sk.json index 1a3e5d67caa..aa68b67f678 100644 --- a/homeassistant/components/econet/translations/sk.json +++ b/homeassistant/components/econet/translations/sk.json @@ -1,15 +1,19 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" } } } diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 165dc49e205..c94afd8b5d7 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -106,7 +106,7 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): return op_list @property - def supported_features(self): + def supported_features(self) -> WaterHeaterEntityFeature: """Return the list of supported features.""" if self.water_heater.modes: if self.water_heater.supports_away: diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 9ba16231867..f6508b4fc57 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "dependencies": ["webhook"], - "requirements": ["aioecowitt==2022.09.3"], + "requirements": ["aioecowitt==2022.11.0"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/homeassistant/components/ecowitt/translations/bg.json b/homeassistant/components/ecowitt/translations/bg.json index 92e4c1888a4..630925a02f5 100644 --- a/homeassistant/components/ecowitt/translations/bg.json +++ b/homeassistant/components/ecowitt/translations/bg.json @@ -1,9 +1,5 @@ { "config": { - "error": { - "invalid_port": "\u041f\u043e\u0440\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430.", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" - }, "step": { "user": { "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Ecowitt?" diff --git a/homeassistant/components/ecowitt/translations/ca.json b/homeassistant/components/ecowitt/translations/ca.json index 5dd00992145..bfd4e1111cd 100644 --- a/homeassistant/components/ecowitt/translations/ca.json +++ b/homeassistant/components/ecowitt/translations/ca.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Per acabar de configurar la integraci\u00f3, utilitza l'aplicaci\u00f3 Ecowitt (al m\u00f2bil) o v\u00e9s a Ecowitt WebUI a trav\u00e9s d'un navegador anant a l'adre\u00e7a IP de l'estaci\u00f3.\n\nTria la teva estaci\u00f3 -> Men\u00fa Altres -> Servidors de pujada DIY. Prem seg\u00fcent (next) i selecciona 'Personalitzat' ('Customized')\n\n- IP del servidor: `{servidor}`\n- Ruta: `{path}`\n- Port: `{port}`\n\nFes clic a 'Desa' ('Save')." }, - "error": { - "invalid_port": "Aquest port ja est\u00e0 en \u00fas.", - "unknown": "Error inesperat" - }, "step": { "user": { - "data": { - "path": "Cam\u00ed amb testimoni de seguretat", - "port": "Port d'escolta" - }, "description": "Est\u00e0s segur que vols configurar Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/cs.json b/homeassistant/components/ecowitt/translations/cs.json deleted file mode 100644 index b9301d0099d..00000000000 --- a/homeassistant/components/ecowitt/translations/cs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "config": { - "error": { - "invalid_port": "Port je ji\u017e pou\u017e\u00edv\u00e1n", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/de.json b/homeassistant/components/ecowitt/translations/de.json index 752363c97e7..37f5c588fcf 100644 --- a/homeassistant/components/ecowitt/translations/de.json +++ b/homeassistant/components/ecowitt/translations/de.json @@ -1,18 +1,10 @@ { "config": { "create_entry": { - "default": "Um die Integration abzuschlie\u00dfen, verwende die Ecowitt App (auf deinem Telefon) oder rufe die Ecowitt WebUI in einem Browser unter der IP-Adresse der Station auf.\n\nW\u00e4hle deine Station -> Men\u00fc Andere -> DIY Upload Servers. Klicke auf \"Weiter\" und w\u00e4hle \"Angepasst\".\n\n- Server IP: `{server}`\n- Pfad: `{path}`\n- Anschluss: `{port}`\n\nKlicke auf \"Speichern\"." - }, - "error": { - "invalid_port": "Port wird bereits verwendet.", - "unknown": "Unerwarteter Fehler" + "default": "Um die Integration abzuschlie\u00dfen, verwende die Ecowitt App (auf deinem Telefon) oder rufe die Ecowitt Web UI in einem Browser unter der IP-Adresse der Station auf.\n\nW\u00e4hle deine Station \u2192 Men\u00fc Andere \u2192 DIY Upload Servers. Klicke auf \"Weiter\" und w\u00e4hle \"Angepasst\".\n\n- Server IP: `{server}`\n- Pfad: `{path}`\n- Anschluss: `{port}`\n\nKlicke auf \"Speichern\"." }, "step": { "user": { - "data": { - "path": "Pfad mit Sicherheits-Token", - "port": "Listening-Port" - }, "description": "M\u00f6chtest du Ecowitt wirklich einrichten?" } } diff --git a/homeassistant/components/ecowitt/translations/el.json b/homeassistant/components/ecowitt/translations/el.json index e8022f8808a..9c5910d71e6 100644 --- a/homeassistant/components/ecowitt/translations/el.json +++ b/homeassistant/components/ecowitt/translations/el.json @@ -3,16 +3,8 @@ "create_entry": { "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt App (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n\n\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 -> \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03bf\u03b9 -> \u0395\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ad\u03c2 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 next \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Customized' (\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf)\n\n- IP \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae: `{server}`\n- \u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae: `{path}`\n- \u0398\u03cd\u03c1\u03b1: `{port}`\n\n\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd '\u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7'." }, - "error": { - "invalid_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, "step": { "user": { - "data": { - "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2" - }, "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03bf\u03cd\u03bd \u03c4\u03b1 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 - > \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03b1 - > \u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY.\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\" \n\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf Ecowitt \u03ba\u03b1\u03b9 \u03b2\u03ac\u03bb\u03c4\u03b5 \u03c4\u03bf ip/hostname \u03c4\u03bf\u03c5 hass server \u03c3\u03b1\u03c2.\n \u0397 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc /.\n \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf Ecowitt \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c3\u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2." } } diff --git a/homeassistant/components/ecowitt/translations/en.json b/homeassistant/components/ecowitt/translations/en.json index 77b50cd6462..b8ce69c10b8 100644 --- a/homeassistant/components/ecowitt/translations/en.json +++ b/homeassistant/components/ecowitt/translations/en.json @@ -3,16 +3,8 @@ "create_entry": { "default": "To finish setting up the integration, use the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\n\nPick your station -> Menu Others -> DIY Upload Servers. Hit next and select 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nClick on 'Save'." }, - "error": { - "invalid_port": "Port is already used.", - "unknown": "Unexpected error" - }, "step": { "user": { - "data": { - "path": "Path with Security token", - "port": "Listening port" - }, "description": "Are you sure you want to set up Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/es.json b/homeassistant/components/ecowitt/translations/es.json index 94e21c4782c..1c7816bc51c 100644 --- a/homeassistant/components/ecowitt/translations/es.json +++ b/homeassistant/components/ecowitt/translations/es.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Para terminar de configurar la integraci\u00f3n, usa la aplicaci\u00f3n Ecowitt (en tu tel\u00e9fono) o accede a Ecowitt WebUI en un navegador en la direcci\u00f3n IP de la estaci\u00f3n. \n\nElige tu estaci\u00f3n - > Men\u00fa Otros - > Servidores de carga de bricolaje. Presiona siguiente y selecciona 'Personalizado' \n\n- IP del servidor: `{server}`\n- Ruta: `{path}`\n- Puerto: `{port}` \n\nHaz clic en 'Guardar'." }, - "error": { - "invalid_port": "El puerto ya est\u00e1 en uso.", - "unknown": "Error inesperado" - }, "step": { "user": { - "data": { - "path": "Ruta con token de seguridad", - "port": "Puerto de escucha" - }, "description": "\u00bfEst\u00e1s seguro de que quieres configurar Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/et.json b/homeassistant/components/ecowitt/translations/et.json index e132191ca37..2e07e97953f 100644 --- a/homeassistant/components/ecowitt/translations/et.json +++ b/homeassistant/components/ecowitt/translations/et.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Sidumise seadistamise l\u00f5petamiseks kasuta Ecowitti rakendust (telefonis) v\u00f5i sisene Ecowitt WebUI-sse brauseris jaama IP-aadressil.\n\nVali oma jaam -> men\u00fc\u00fc Muud -> DIY Upload Servers. Vajuta nuppu next ja vali 'Customized' (kohandatud)\n\n- Serveri IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nVajuta nupule 'Save'." }, - "error": { - "invalid_port": "Port on juba kasutusel.", - "unknown": "Ootamatu t\u00f5rge" - }, "step": { "user": { - "data": { - "path": "Turvam\u00e4rgiga asukoht", - "port": "Kuulamisport" - }, "description": "Kas oled kindel, et soovid Ecowitti seadistada?" } } diff --git a/homeassistant/components/ecowitt/translations/fr.json b/homeassistant/components/ecowitt/translations/fr.json index c4f3bfbb937..c22d5aec5a7 100644 --- a/homeassistant/components/ecowitt/translations/fr.json +++ b/homeassistant/components/ecowitt/translations/fr.json @@ -1,14 +1,7 @@ { "config": { - "error": { - "invalid_port": "Le port est d\u00e9j\u00e0 utilis\u00e9.", - "unknown": "Erreur inattendue" - }, "step": { "user": { - "data": { - "port": "Port d'\u00e9coute" - }, "description": "Voulez-vous vraiment configurer Ecowitt\u00a0?" } } diff --git a/homeassistant/components/ecowitt/translations/he.json b/homeassistant/components/ecowitt/translations/he.json deleted file mode 100644 index 822dcf2be14..00000000000 --- a/homeassistant/components/ecowitt/translations/he.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/hu.json b/homeassistant/components/ecowitt/translations/hu.json index 920602311bf..90b289d3257 100644 --- a/homeassistant/components/ecowitt/translations/hu.json +++ b/homeassistant/components/ecowitt/translations/hu.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Az integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1nak befejez\u00e9s\u00e9hez haszn\u00e1lja az Ecowitt alkalmaz\u00e1st (a telefonj\u00e1n), vagy l\u00e9pjen be az Ecowitt WebUI-ba egy b\u00f6ng\u00e9sz\u0151ben az \u00e1llom\u00e1s IP-c\u00edm\u00e9n.\n\nV\u00e1lassza ki az \u00e1llom\u00e1s\u00e1t -> 'Others' men\u00fc -> 'DIY Upload Servers'. Nyomja meg a 'Next' gombot, \u00e9s v\u00e1lassza a 'Customized' lehet\u0151s\u00e9get.\n\n- Szerver IP: `{server}`\n- \u00datvonal: `{path}`\n- Port: `{port}`\n\nKattintson a 'Save' gombra." }, - "error": { - "invalid_port": "A port m\u00e1r haszn\u00e1latban van.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, "step": { "user": { - "data": { - "path": "Biztons\u00e1gi tokennel ell\u00e1tott el\u00e9r\u00e9si \u00fatvonal", - "port": "Figyel\u0151port" - }, "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani: Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/id.json b/homeassistant/components/ecowitt/translations/id.json index 36479f19729..60f71335e10 100644 --- a/homeassistant/components/ecowitt/translations/id.json +++ b/homeassistant/components/ecowitt/translations/id.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Untuk menyelesaikan pengaturan integrasi, gunakan Ecowitt App (pada ponsel Anda) atau akses Ecowitt WebUI di browser pada alamat IP stasiun.\n\nPilih stasiun Anda -> Menu Others -> DIY Upload Servers. Tekan 'Next' dan pilih 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nKlik 'Simpan'." }, - "error": { - "invalid_port": "Port sudah digunakan.", - "unknown": "Kesalahan yang tidak diharapkan" - }, "step": { "user": { - "data": { - "path": "Jalur dengan token Keamanan", - "port": "Port mendengarkan" - }, "description": "Yakin ingin menyiapkan Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/it.json b/homeassistant/components/ecowitt/translations/it.json index 91cfcd52fe2..1cc82c0cf0e 100644 --- a/homeassistant/components/ecowitt/translations/it.json +++ b/homeassistant/components/ecowitt/translations/it.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Per completare la configurazione dell'integrazione, utilizzare l'App Ecowitt (sul telefono) o accedere all'Ecowitt WebUI in un browser all'indirizzo IP della stazione. \n\nScegli la tua stazione - > Menu Altri - > Server di caricamento fai-da-te. Premi Avanti e seleziona \"Personalizzata\" \n\n - IP del server: `{server}`\n - Percorso: `{path}`\n - Porta: `{port}` \n\n Fai clic su \"Salva\"." }, - "error": { - "invalid_port": "La porta \u00e8 gi\u00e0 utilizzata.", - "unknown": "Errore imprevisto" - }, "step": { "user": { - "data": { - "path": "Percorso con token di sicurezza", - "port": "Porta di ascolto" - }, "description": "Sei sicuro di voler configurare Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/ja.json b/homeassistant/components/ecowitt/translations/ja.json index 0853f5068b1..fed7a566b3b 100644 --- a/homeassistant/components/ecowitt/translations/ja.json +++ b/homeassistant/components/ecowitt/translations/ja.json @@ -3,16 +3,8 @@ "create_entry": { "default": "\u7d71\u5408\u306e\u8a2d\u5b9a\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001Ecowitt \u30a2\u30d7\u30ea (\u96fb\u8a71\u3067) \u3092\u4f7f\u7528\u3059\u308b\u304b\u3001\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e IP \u30a2\u30c9\u30ec\u30b9\u3067\u30d6\u30e9\u30a6\u30b6\u30fc\u3067 Ecowitt WebUI \u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059\u3002 \n\n\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u9078\u629e - >\u30e1\u30cb\u30e5\u30fc\u306e [\u305d\u306e\u4ed6] - > DIY \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9 \u30b5\u30fc\u30d0\u30fc] \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u6b21\u306b\u30d2\u30c3\u30c8\u3057\u3001\u300c\u30ab\u30b9\u30bf\u30de\u30a4\u30ba\u300d\u3092\u9078\u629e\u3057\u307e\u3059\n\n - \u30b5\u30fc\u30d0\u30fc IP: ` {server} `\n - \u30d1\u30b9: ` {path} `\n - \u30dd\u30fc\u30c8: ` {port} ` \n\n \u300c\u4fdd\u5b58\u300d\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" }, - "error": { - "invalid_port": "\u30dd\u30fc\u30c8\u306f\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, "step": { "user": { - "data": { - "path": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30c8\u30fc\u30af\u30f3\u3092\u542b\u3080\u30d1\u30b9", - "port": "\u30ea\u30b9\u30cb\u30f3\u30b0\u30dd\u30fc\u30c8" - }, "description": "Ecowitt\u3092\u3001\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f" } } diff --git a/homeassistant/components/ecowitt/translations/nb.json b/homeassistant/components/ecowitt/translations/nb.json deleted file mode 100644 index a22f7eef3d6..00000000000 --- a/homeassistant/components/ecowitt/translations/nb.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "unknown": "Uventet feil" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/nl.json b/homeassistant/components/ecowitt/translations/nl.json index 112651607f4..67f34ac63aa 100644 --- a/homeassistant/components/ecowitt/translations/nl.json +++ b/homeassistant/components/ecowitt/translations/nl.json @@ -1,9 +1,5 @@ { "config": { - "error": { - "invalid_port": "Poort wordt al gebruikt.", - "unknown": "Onverwachte fout" - }, "step": { "user": { "description": "Weet u zeker dat u Ecowitt wilt instellen?" diff --git a/homeassistant/components/ecowitt/translations/no.json b/homeassistant/components/ecowitt/translations/no.json index 61372b6f49f..e41c2ef373c 100644 --- a/homeassistant/components/ecowitt/translations/no.json +++ b/homeassistant/components/ecowitt/translations/no.json @@ -3,16 +3,8 @@ "create_entry": { "default": "For \u00e5 fullf\u00f8re konfigureringen av integrasjonen, bruk Ecowitt-appen (p\u00e5 telefonen) eller g\u00e5 til Ecowitt WebUI i en nettleser p\u00e5 stasjonens IP-adresse. \n\n Velg stasjonen din - > Meny Andre - > DIY-opplastingsservere. Trykk neste og velg \"Tilpasset\" \n\n - Server IP: ` {server} `\n - Bane: ` {path} `\n - Port: ` {port} ` \n\n Klikk p\u00e5 'Lagre'." }, - "error": { - "invalid_port": "Porten er allerede i bruk.", - "unknown": "Uventet feil" - }, "step": { "user": { - "data": { - "path": "Bane med sikkerhetstoken", - "port": "Lytteport" - }, "description": "Er du sikker p\u00e5 at du vil sette opp Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/pl.json b/homeassistant/components/ecowitt/translations/pl.json index 64fb3e6e4ef..88769e0a810 100644 --- a/homeassistant/components/ecowitt/translations/pl.json +++ b/homeassistant/components/ecowitt/translations/pl.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Aby zako\u0144czy\u0107 konfiguracj\u0119 integracji, u\u017cyj aplikacji Ecowitt (na telefonie) lub uzyskaj dost\u0119p do Ecowitt WebUI w przegl\u0105darce pod adresem IP stacji. \n\nWybierz swoj\u0105 stacj\u0119 - > Menu \"Others\" - > DIY Upload Servers. Kliknij dalej i wybierz \"Customized\" \n\n- IP serwera: `{server}`\n- \u015acie\u017cka: `{path}`\n- Port: `{port}` \n\nKliknij \u201eZapisz\u201d." }, - "error": { - "invalid_port": "Port jest ju\u017c u\u017cywany.", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, "step": { "user": { - "data": { - "path": "\u015acie\u017cka do tokena bezpiecze\u0144stwa", - "port": "Port nas\u0142uchiwania" - }, "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" } } diff --git a/homeassistant/components/ecowitt/translations/pt-BR.json b/homeassistant/components/ecowitt/translations/pt-BR.json index b0c23d7a35d..bfe6c893356 100644 --- a/homeassistant/components/ecowitt/translations/pt-BR.json +++ b/homeassistant/components/ecowitt/translations/pt-BR.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Para finalizar a configura\u00e7\u00e3o da integra\u00e7\u00e3o, use o app Ecowitt (no seu smartphone) ou acesse o Ecowitt WebUI em um navegador no endere\u00e7o IP da esta\u00e7\u00e3o. \n\n Escolha sua esta\u00e7\u00e3o - > Menu Outros - > Servidores de Upload DIY. Clique em pr\u00f3ximo e selecione 'Personalizado' \n\n - IP do servidor: `{server}`\n - Caminho: `{path}`\n - Porta: `{port}` \n\n Clique em 'Salvar'." }, - "error": { - "invalid_port": "A porta j\u00e1 \u00e9 usada.", - "unknown": "Erro inesperado" - }, "step": { "user": { - "data": { - "path": "Caminho com token de seguran\u00e7a", - "port": "Porta de escuta" - }, "description": "Tem certeza de que deseja configurar o Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/pt.json b/homeassistant/components/ecowitt/translations/pt.json index 71a66816a83..366abdcfef0 100644 --- a/homeassistant/components/ecowitt/translations/pt.json +++ b/homeassistant/components/ecowitt/translations/pt.json @@ -3,15 +3,8 @@ "create_entry": { "default": "Para finalizar a configura\u00e7\u00e3o da integra\u00e7\u00e3o, use o Ecowitt App (no seu telefone) ou acesse o Ecowitt WebUI em um navegador no endere\u00e7o IP da esta\u00e7\u00e3o. \n\n Escolha sua esta\u00e7\u00e3o - > Menu Outros - > Servidores de Upload DIY. Clique em pr\u00f3ximo e selecione 'Personalizado' \n\n - IP do servidor: ` {server} `\n - Caminho: ` {path} `\n - Porta: ` {port} ` \n\n Clique em 'Salvar'." }, - "error": { - "invalid_port": "A porta j\u00e1 \u00e9 usada." - }, "step": { "user": { - "data": { - "path": "Caminho com token de seguran\u00e7a", - "port": "Porta de escuta" - }, "description": "Tem certeza de que deseja configurar o Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/ru.json b/homeassistant/components/ecowitt/translations/ru.json index 97532b0726b..2299e98db0b 100644 --- a/homeassistant/components/ecowitt/translations/ru.json +++ b/homeassistant/components/ecowitt/translations/ru.json @@ -3,16 +3,8 @@ "create_entry": { "default": "\u0427\u0442\u043e\u0431\u044b \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Ecowitt (\u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435) \u0438\u043b\u0438 \u0432\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Ecowitt \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 \u043f\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0441\u0442\u0430\u043d\u0446\u0438\u0438. \n\n\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0432\u043e\u044e \u0441\u0442\u0430\u043d\u0446\u0438\u044e - > \u041c\u0435\u043d\u044e 'Others' - > 'DIY Upload Servers'. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 'Next' \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 'Customized'. \n\n- IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: `{server}`\n- \u041f\u0443\u0442\u044c: `{path}`\n- \u041f\u043e\u0440\u0442: `{port}` \n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 'Save'." }, - "error": { - "invalid_port": "\u041f\u043e\u0440\u0442 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\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": { - "path": "\u041f\u0443\u0442\u044c \u0441 \u0442\u043e\u043a\u0435\u043d\u043e\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438", - "port": "\u041f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f" - }, "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 Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/sv.json b/homeassistant/components/ecowitt/translations/sv.json index 0edd1d70fa9..830741f4403 100644 --- a/homeassistant/components/ecowitt/translations/sv.json +++ b/homeassistant/components/ecowitt/translations/sv.json @@ -3,16 +3,8 @@ "create_entry": { "default": "F\u00f6r att avsluta inst\u00e4llningen av integrationen, anv\u00e4nd Ecowitt-appen (p\u00e5 din telefon) eller g\u00e5 till Ecowitt WebUI i en webbl\u00e4sare p\u00e5 stationens IP-adress. \n\n V\u00e4lj din station - > Meny \u00d6vriga - > DIY Upload Servers. Klicka p\u00e5 n\u00e4sta och v\u00e4lj \"Anpassad\" \n\n - Server-IP: ` {server} `\n - S\u00f6kv\u00e4g: ` {path} `\n - Port: ` {port} ` \n\n Klicka p\u00e5 'Spara'." }, - "error": { - "invalid_port": "Porten anv\u00e4nds redan.", - "unknown": "Ov\u00e4ntat fel" - }, "step": { "user": { - "data": { - "path": "S\u00f6kv\u00e4g med s\u00e4kerhetstoken", - "port": "Lyssningsport" - }, "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/tr.json b/homeassistant/components/ecowitt/translations/tr.json index 8e4d6906e4b..3c529f0c856 100644 --- a/homeassistant/components/ecowitt/translations/tr.json +++ b/homeassistant/components/ecowitt/translations/tr.json @@ -3,16 +3,8 @@ "create_entry": { "default": "Entegrasyon kurulumunu tamamlamak i\u00e7in Ecowitt Uygulamas\u0131n\u0131 (telefonunuzda) kullan\u0131n veya istasyonun IP adresindeki bir taray\u0131c\u0131da Ecowitt WebUI'ye eri\u015fin. \n\n \u0130stasyonunuzu se\u00e7in - > Men\u00fc Di\u011ferleri - > Kendin Yap Y\u00fckleme Sunucular\u0131. \u0130leri'ye bas\u0131n ve '\u00d6zelle\u015ftirilmi\u015f'i se\u00e7in \n\n - Sunucu IP'si: ` {server} `\n - Yol: ` {path} `\n - Ba\u011flant\u0131 noktas\u0131: ` {port} ` \n\n 'Kaydet'e t\u0131klay\u0131n." }, - "error": { - "invalid_port": "Ba\u011flant\u0131 noktas\u0131 zaten kullan\u0131l\u0131yor.", - "unknown": "Beklenmeyen hata" - }, "step": { "user": { - "data": { - "path": "G\u00fcvenlik anahtar\u0131 i\u00e7eren yol", - "port": "Dinleme ba\u011flant\u0131 noktas\u0131" - }, "description": "Ecowitt'i kurmak istedi\u011finizden emin misiniz?" } } diff --git a/homeassistant/components/ecowitt/translations/zh-Hant.json b/homeassistant/components/ecowitt/translations/zh-Hant.json index 3ad87a5733b..69373927626 100644 --- a/homeassistant/components/ecowitt/translations/zh-Hant.json +++ b/homeassistant/components/ecowitt/translations/zh-Hant.json @@ -3,16 +3,8 @@ "create_entry": { "default": "\u5fc5\u9808\u57f7\u884c\u4ee5\u4e0b\u6b65\u9a5f\u4ee5\u8a2d\u5b9a\u6b64\u6574\u5408\u3001\u65bc\u624b\u6a5f\u4e0a\u4f7f\u7528 Ecowitt App \u6216\u4f7f\u7528\u700f\u89bd\u5668\u8f38\u5165\u7ad9\u9ede IP \u4f4d\u5740\u9032\u5165 Ecowitt WebUI\u3002\n\n\u9078\u64c7\u7ad9\u9ede -> \u9078\u55ae\u4e2d\u5176\u4ed6 -> DIY \u4e0a\u50b3\u4f3a\u670d\u5668\u3001\u9ede\u9078\u4e0b\u4e00\u6b65\u4e26\u9078\u64c7 '\u81ea\u8a02'\n\n- \u4f3a\u670d\u5668 IP\uff1a`{server}`\n- \u8def\u5f91\uff1a`{path}`\n- \u901a\u8a0a\u57e0\uff1a`{port}`\n\n\u9ede\u9078 '\u5132\u5b58'\u3002" }, - "error": { - "invalid_port": "\u901a\u8a0a\u57e0\u5df2\u88ab\u4f7f\u7528\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, "step": { "user": { - "data": { - "path": "\u52a0\u5bc6\u6b0a\u6756\u8def\u5f91", - "port": "\u76e3\u807d\u901a\u8a0a\u57e0" - }, "description": "\u662f\u5426\u8981\u8a2d\u5b9a Ecowitt\uff1f" } } diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index fc90591cae6..b0cff7e203f 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pyefergy==22.1.1"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "loggers": ["iso4217", "pyefergy"] + "loggers": ["iso4217", "pyefergy"], + "integration_type": "hub" } diff --git a/homeassistant/components/efergy/translations/bg.json b/homeassistant/components/efergy/translations/bg.json index 14d4c77c8f9..2abb443e903 100644 --- a/homeassistant/components/efergy/translations/bg.json +++ b/homeassistant/components/efergy/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/efergy/translations/sk.json b/homeassistant/components/efergy/translations/sk.json index 64731388e98..8aa53bb641e 100644 --- a/homeassistant/components/efergy/translations/sk.json +++ b/homeassistant/components/efergy/translations/sk.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index b07865d8591..58648123dcf 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -7,9 +7,13 @@ from typing import Any from pyeight.eight import EightSleep import voluptuous as vol -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform as ep from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -175,10 +179,13 @@ class EightUserSensor(EightSleepBaseEntity, SensorEntity): if self._sensor == "bed_temperature": self._attr_icon = "mdi:thermometer" self._attr_device_class = SensorDeviceClass.TEMPERATURE - self._attr_native_unit_of_measurement = TEMP_CELSIUS + self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS elif self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"): self._attr_native_unit_of_measurement = "Score" + if self._sensor != "sleep_stage": + self._attr_state_class = SensorStateClass.MEASUREMENT + _LOGGER.debug( "User Sensor: %s, Side: %s, User: %s", self._sensor, @@ -272,7 +279,8 @@ class EightRoomSensor(EightSleepBaseEntity, SensorEntity): _attr_icon = "mdi:thermometer" _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS def __init__( self, diff --git a/homeassistant/components/eight_sleep/translations/sk.json b/homeassistant/components/eight_sleep/translations/sk.json new file mode 100644 index 00000000000..ef0fd942acb --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/sk.json b/homeassistant/components/elgato/translations/sk.json index 892b8b2cd91..d44c8833bc3 100644 --- a/homeassistant/components/elgato/translations/sk.json +++ b/homeassistant/components/elgato/translations/sk.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{serial_number}", "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/elkm1/translations/bg.json b/homeassistant/components/elkm1/translations/bg.json index 46a60e96408..5a7a68927a8 100644 --- a/homeassistant/components/elkm1/translations/bg.json +++ b/homeassistant/components/elkm1/translations/bg.json @@ -6,6 +6,7 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "flow_title": "{mac_address} ({host})", diff --git a/homeassistant/components/elkm1/translations/de.json b/homeassistant/components/elkm1/translations/de.json index 7c08a9b254d..976cd88938d 100644 --- a/homeassistant/components/elkm1/translations/de.json +++ b/homeassistant/components/elkm1/translations/de.json @@ -22,7 +22,7 @@ "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit.", "username": "Benutzername" }, - "description": "Verbinde dich mit dem ermittelten System: {mac_address} ( {host} )", + "description": "Verbinde dich mit dem ermittelten System: {mac_address} ({host})", "title": "Stelle eine Verbindung zur Elk-M1-Steuerung her" }, "manual_connection": { diff --git a/homeassistant/components/elkm1/translations/hr.json b/homeassistant/components/elkm1/translations/hr.json new file mode 100644 index 00000000000..06224788ca6 --- /dev/null +++ b/homeassistant/components/elkm1/translations/hr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neva\u017ee\u0107a provjera autenti\u010dnosti", + "unknown": "Neo\u010dekivana gre\u0161ka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/sk.json b/homeassistant/components/elkm1/translations/sk.json index 0b7bf878ea9..3ec7110ece0 100644 --- a/homeassistant/components/elkm1/translations/sk.json +++ b/homeassistant/components/elkm1/translations/sk.json @@ -1,10 +1,45 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{mac_address} ({host})", + "step": { + "discovered_connection": { + "data": { + "password": "Heslo", + "protocol": "Protokol", + "temperature_unit": "Jednotka teploty pou\u017e\u00edva ElkM1.", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Pripojte sa k objaven\u00e9mu syst\u00e9mu: {mac_address} ({host})", + "title": "Pripojte k Elk-M1 Control" + }, + "manual_connection": { + "data": { + "address": "IP adresa alebo dom\u00e9na alebo s\u00e9riov\u00fd port, ak sa prip\u00e1jate cez s\u00e9riov\u00fd port.", + "password": "Heslo", + "prefix": "Jedine\u010dn\u00e1 predpona (ak m\u00e1te iba jeden ElkM1, nechajte pr\u00e1zdne).", + "protocol": "Protokol", + "temperature_unit": "Jednotka teploty pou\u017e\u00edva ElkM1.", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripojte k Elk-M1 Control" + }, + "user": { + "data": { + "device": "Zariadenie" + }, + "title": "Pripojte k Elk-M1 Control" + } } } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/sk.json b/homeassistant/components/elmax/translations/sk.json index 5ada995aa6e..43c7507b1b9 100644 --- a/homeassistant/components/elmax/translations/sk.json +++ b/homeassistant/components/elmax/translations/sk.json @@ -1,7 +1,27 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_pin": "Poskytnut\u00fd k\u00f3d PIN je neplatn\u00fd", + "network_error": "Vyskytla sa chyba siete", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "panels": { + "data": { + "panel_id": "ID panela", + "panel_pin": "PIN k\u00f3d" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index b573aef65ea..bc85b983d9d 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -284,11 +284,11 @@ class EmbyDevice(MediaPlayerEntity): return self.device.media_album_artist @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self.supports_remote_control: return SUPPORT_EMBY - return 0 + return MediaPlayerEntityFeature(0) async def async_media_play(self) -> None: """Play media.""" diff --git a/homeassistant/components/emonitor/translations/sk.json b/homeassistant/components/emonitor/translations/sk.json new file mode 100644 index 00000000000..323aa28cafb --- /dev/null +++ b/homeassistant/components/emonitor/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Chcete nastavi\u0165 {name} ({host})?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 31319a656cd..86e8f2fc2ca 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.10.4"], + "requirements": ["sense_energy==0.11.0"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/emulated_roku/translations/sk.json b/homeassistant/components/emulated_roku/translations/sk.json index af15f92c2f2..96ca4e8f40a 100644 --- a/homeassistant/components/emulated_roku/translations/sk.json +++ b/homeassistant/components/emulated_roku/translations/sk.json @@ -1,10 +1,15 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "step": { "user": { "data": { + "host_ip": "IP adresa hostite\u013ea", "name": "N\u00e1zov" - } + }, + "title": "Definujte konfigur\u00e1ciu servera" } } } diff --git a/homeassistant/components/energy/translations/sk.json b/homeassistant/components/energy/translations/sk.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/sk.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index c2b693c0809..293d34ae8d8 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -272,8 +272,8 @@ async def ws_get_fossil_energy_consumption( end_time, statistic_ids, "hour", - True, {"energy": UnitOfEnergy.KILO_WATT_HOUR}, + {"mean", "sum"}, ) def _combine_sum_statistics( diff --git a/homeassistant/components/enocean/translations/sk.json b/homeassistant/components/enocean/translations/sk.json new file mode 100644 index 00000000000..a550ef6b29a --- /dev/null +++ b/homeassistant/components/enocean/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Nespr\u00e1vna cesta k hardv\u00e9rov\u00e9mu k\u013e\u00fa\u010du", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "invalid_dongle_path": "Pre t\u00fato cestu nebol n\u00e1jden\u00fd \u017eiadny platn\u00fd k\u013e\u00fa\u010d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/bg.json b/homeassistant/components/enphase_envoy/translations/bg.json index 7d794942093..53a947fb298 100644 --- a/homeassistant/components/enphase_envoy/translations/bg.json +++ b/homeassistant/components/enphase_envoy/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/enphase_envoy/translations/sk.json b/homeassistant/components/enphase_envoy/translations/sk.json index 71a7aea5018..90056cd5864 100644 --- a/homeassistant/components/enphase_envoy/translations/sk.json +++ b/homeassistant/components/enphase_envoy/translations/sk.json @@ -1,10 +1,23 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{serial} ( {host} )", + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/environment_canada/diagnostics.py b/homeassistant/components/environment_canada/diagnostics.py new file mode 100644 index 00000000000..f1064cea52e --- /dev/null +++ b/homeassistant/components/environment_canada/diagnostics.py @@ -0,0 +1,26 @@ +"""Diagnostics support for Environment Canada.""" +from __future__ import annotations + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +TO_REDACT = {CONF_LATITUDE, CONF_LONGITUDE} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinators = hass.data[DOMAIN][config_entry.entry_id] + weather_coord = coordinators["weather_coordinator"] + + diagnostics_data = { + "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), + "weather_data": dict(weather_coord.ec_data.conditions), + } + + return diagnostics_data diff --git a/homeassistant/components/environment_canada/translations/sk.json b/homeassistant/components/environment_canada/translations/sk.json index e6945904d90..7558d93d309 100644 --- a/homeassistant/components/environment_canada/translations/sk.json +++ b/homeassistant/components/environment_canada/translations/sk.json @@ -1,10 +1,18 @@ { "config": { + "error": { + "bad_station_id": "ID stanice je neplatn\u00e9, ch\u00fdba alebo sa nenach\u00e1dza v datab\u00e1ze ID stanice", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "too_many_attempts": "Spojenie s Environment Canada m\u00e1 obmedzen\u00fa r\u00fdchlos\u0165; Sk\u00faste to znova o 60 sek\u00fand", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { + "language": "Jazyk inform\u00e1ci\u00ed o po\u010das\u00ed", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "station": "ID meteorologickej stanice" } } } diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 8dbf8c15731..74bf9c8ca54 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -25,10 +25,10 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - LENGTH_KILOMETERS, - PRESSURE_KPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,10 +70,10 @@ class ECWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" _attr_has_entity_name = True - _attr_native_pressure_unit = PRESSURE_KPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_visibility_unit = LENGTH_KILOMETERS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_pressure_unit = UnitOfPressure.KPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_visibility_unit = UnitOfLength.KILOMETERS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__(self, coordinator, hourly): """Initialize Environment Canada weather.""" diff --git a/homeassistant/components/epson/translations/bg.json b/homeassistant/components/epson/translations/bg.json index a051d6ca487..d2c9013bcc5 100644 --- a/homeassistant/components/epson/translations/bg.json +++ b/homeassistant/components/epson/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/epson/translations/sk.json b/homeassistant/components/epson/translations/sk.json index af15f92c2f2..1820d9d8d42 100644 --- a/homeassistant/components/epson/translations/sk.json +++ b/homeassistant/components/epson/translations/sk.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov" } } diff --git a/homeassistant/components/escea/translations/he.json b/homeassistant/components/escea/translations/he.json new file mode 100644 index 00000000000..032c9c9fa17 --- /dev/null +++ b/homeassistant/components/escea/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/sk.json b/homeassistant/components/escea/translations/sk.json new file mode 100644 index 00000000000..99798036ffd --- /dev/null +++ b/homeassistant/components/escea/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 23b6a6550e4..3315711f4ad 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -24,10 +24,11 @@ from aioesphomeapi import ( UserService, UserServiceArgType, ) +from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant import const -from homeassistant.components import zeroconf +from homeassistant.components import tag, zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -46,6 +47,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template @@ -59,6 +65,37 @@ CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) _R = TypeVar("_R") +STABLE_BLE_VERSION_STR = "2022.11.0" +STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) + + +@callback +def _async_check_firmware_version( + hass: HomeAssistant, device_info: EsphomeDeviceInfo +) -> None: + """Create or delete an the ble_firmware_outdated issue.""" + # ESPHome device_info.name is the unique_id + issue = f"ble_firmware_outdated-{device_info.name}" + if ( + not device_info.bluetooth_proxy_version + # If the device has a project name its up to that project + # to tell them about the firmware version update so we don't notify here + or device_info.project_name + or AwesomeVersion(device_info.esphome_version) >= STABLE_BLE_VERSION + ): + async_delete_issue(hass, DOMAIN, issue) + return + async_create_issue( + hass, + DOMAIN, + issue, + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url=f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html", + translation_key="ble_firmware_outdated", + translation_placeholders={"name": device_info.name}, + ) + async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry @@ -109,8 +146,7 @@ async def async_setup_entry( # noqa: C901 if service.data_template: try: data_template = { - key: Template(value) # type: ignore[no-untyped-call] - for key, value in service.data_template.items() + key: Template(value) for key, value in service.data_template.items() } template.attach(hass, data_template) service_data.update( @@ -131,11 +167,8 @@ async def async_setup_entry( # noqa: C901 # Call native tag scan if service_name == "tag_scanned" and device_id is not None: - # Importing tag via hass.components in case it is overridden - # in a custom_components (custom_components.tag) - tag = hass.components.tag tag_id = service_data["tag_id"] - hass.async_create_task(tag.async_scan_tag(tag_id, device_id)) + hass.async_create_task(tag.async_scan_tag(hass, tag_id, device_id)) return hass.bus.async_fire( @@ -218,7 +251,8 @@ async def async_setup_entry( # noqa: C901 """Subscribe to states and list entities on successful API login.""" nonlocal device_id try: - entry_data.device_info = await cli.device_info() + device_info = await cli.device_info() + entry_data.device_info = device_info assert cli.api_version is not None entry_data.api_version = cli.api_version entry_data.available = True @@ -246,6 +280,8 @@ async def async_setup_entry( # noqa: C901 _LOGGER.warning("Error getting initial data for %s: %s", host, err) # Re-connection logic will trigger after this await cli.disconnect() + else: + _async_check_firmware_version(hass, device_info) async def on_disconnect() -> None: """Run disconnect callbacks on API disconnect.""" diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index ffe322b6259..96ad9d05238 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -1,9 +1,14 @@ """Support for ESPHome binary sensors.""" from __future__ import annotations +from contextlib import suppress + from aioesphomeapi import BinarySensorInfo, BinarySensorState -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,11 +50,11 @@ class EsphomeBinarySensor( return self._state.state @property - def device_class(self) -> str | None: + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this device, from component DEVICE_CLASSES.""" - if self._static_info.device_class not in DEVICE_CLASSES: - return None - return self._static_info.device_class + with suppress(ValueError): + return BinarySensorDeviceClass(self._static_info.device_class) + return None @property def available(self) -> bool: diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index b5be5362474..4b6281c7c5a 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -67,7 +67,9 @@ async def async_connect_scanner( source=source, can_connect=_async_can_connect_factory(entry_data, source), ) - scanner = ESPHomeScanner(hass, source, new_info_callback, connector, connectable) + scanner = ESPHomeScanner( + hass, source, entry.title, new_info_callback, connector, connectable + ) unload_callbacks = [ async_register_scanner(hass, scanner, connectable), scanner.async_setup(), diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index bd2f0953c27..541eb831ca5 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -22,7 +22,8 @@ from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakError -from homeassistant.core import CALLBACK_TYPE +from homeassistant.components.bluetooth import async_scanner_by_source +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from ..domain_data import DomainData from .characteristic import BleakGATTCharacteristicESPHome @@ -35,6 +36,13 @@ DISCONNECT_TIMEOUT = 5.0 CONNECT_FREE_SLOT_TIMEOUT = 2.0 GATT_READ_TIMEOUT = 30.0 +# CCCD (Characteristic Client Config Descriptor) +CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb" +CCCD_NOTIFY_BYTES = b"\x01\x00" +CCCD_INDICATE_BYTES = b"\x02\x00" + +MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE = 3 + DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE _LOGGER = logging.getLogger(__name__) @@ -95,15 +103,17 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: # Because callbacks are delivered asynchronously it's possible # that we find out about the disconnection during the operation # before the callback is delivered. + + # pylint: disable=protected-access if ex.error.error == -1: _LOGGER.debug( "%s: %s - %s: BLE device disconnected during %s operation", - self._source, # pylint: disable=protected-access - self._ble_device.name, # pylint: disable=protected-access - self._ble_device.address, # pylint: disable=protected-access + self._source, + self._ble_device.name, + self._ble_device.address, func.__name__, ) - self._async_ble_device_disconnected() # pylint: disable=protected-access + self._async_ble_device_disconnected() raise BleakError(str(ex)) from ex except APIConnectionError as err: raise BleakError(str(err)) from err @@ -120,19 +130,26 @@ class ESPHomeClient(BaseBleakClient): """Initialize the ESPHomeClient.""" assert isinstance(address_or_ble_device, BLEDevice) super().__init__(address_or_ble_device, *args, **kwargs) + self._hass: HomeAssistant = kwargs["hass"] self._ble_device = address_or_ble_device self._address_as_int = mac_to_int(self._ble_device.address) assert self._ble_device.details is not None self._source = self._ble_device.details["source"] - self.domain_data = DomainData.get(kwargs["hass"]) + self.domain_data = DomainData.get(self._hass) config_entry = self.domain_data.get_by_unique_id(self._source) self.entry_data = self.domain_data.get_entry_data(config_entry) self._client = self.entry_data.client self._is_connected = False self._mtu: int | None = None self._cancel_connection_state: CALLBACK_TYPE | None = None - self._notify_cancels: dict[int, Callable[[], Coroutine[Any, Any, None]]] = {} + self._notify_cancels: dict[ + int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]] + ] = {} self._disconnected_event: asyncio.Event | None = None + device_info = self.entry_data.device_info + assert device_info is not None + self._connection_version = device_info.bluetooth_proxy_version + self._address_type = address_or_ble_device.details["address_type"] def __str__(self) -> str: """Return the string representation of the client.""" @@ -154,15 +171,22 @@ class ESPHomeClient(BaseBleakClient): ) self._cancel_connection_state = None - def _async_ble_device_disconnected(self) -> None: - """Handle the BLE device disconnecting from the ESP.""" - was_connected = self._is_connected + def _async_disconnected_cleanup(self) -> None: + """Clean up on disconnect.""" self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] self._is_connected = False + for _, notify_abort in self._notify_cancels.values(): + notify_abort() self._notify_cancels.clear() if self._disconnected_event: self._disconnected_event.set() self._disconnected_event = None + self._unsubscribe_connection_state() + + def _async_ble_device_disconnected(self) -> None: + """Handle the BLE device disconnecting from the ESP.""" + was_connected = self._is_connected + self._async_disconnected_cleanup() if was_connected: _LOGGER.debug( "%s: %s - %s: BLE device disconnected", @@ -171,7 +195,6 @@ class ESPHomeClient(BaseBleakClient): self._ble_device.address, ) self._async_call_bleak_disconnected_callback() - self._unsubscribe_connection_state() def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from hass.""" @@ -202,7 +225,14 @@ class ESPHomeClient(BaseBleakClient): Boolean representing connection status. """ await self._wait_for_free_connection_slot(CONNECT_FREE_SLOT_TIMEOUT) - + entry_data = self.entry_data + self._mtu = entry_data.get_gatt_mtu_cache(self._address_as_int) + has_cache = bool( + dangerous_use_bleak_cache + and self._connection_version >= MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE + and entry_data.get_gatt_services_cache(self._address_as_int) + and self._mtu + ) connected_future: asyncio.Future[bool] = asyncio.Future() def _on_bluetooth_connection_state( @@ -220,7 +250,9 @@ class ESPHomeClient(BaseBleakClient): ) if connected: self._is_connected = True - self._mtu = mtu + if not self._mtu: + self._mtu = mtu + entry_data.set_gatt_mtu_cache(self._address_as_int, mtu) else: self._async_ble_device_disconnected() @@ -254,24 +286,58 @@ class ESPHomeClient(BaseBleakClient): self._ble_device.name, self._ble_device.address, ) - self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) + entry_data.disconnect_callbacks.append(self._async_esp_disconnected) connected_future.set_result(connected) timeout = kwargs.get("timeout", self._timeout) - self._cancel_connection_state = await self._client.bluetooth_device_connect( - self._address_as_int, - _on_bluetooth_connection_state, - timeout=timeout, - ) - await connected_future - await self.get_services(dangerous_use_bleak_cache=dangerous_use_bleak_cache) + if not (scanner := async_scanner_by_source(self._hass, self._source)): + raise BleakError("Scanner disappeared for {self._source}") + with scanner.connecting(): + try: + self._cancel_connection_state = ( + await self._client.bluetooth_device_connect( + self._address_as_int, + _on_bluetooth_connection_state, + timeout=timeout, + has_cache=has_cache, + version=self._connection_version, + address_type=self._address_type, + ) + ) + except Exception: + with contextlib.suppress(BleakError): + # If the connect call throws an exception, + # we need to make sure we await the future + # to avoid a warning about an un-retrieved + # exception since we prefer to raise the + # exception from the connect call as it + # will be more descriptive. + if connected_future.done(): + await connected_future + connected_future.cancel() + raise + await connected_future + + try: + await self.get_services(dangerous_use_bleak_cache=dangerous_use_bleak_cache) + except asyncio.CancelledError: + # On cancel we must still raise cancelled error + # to avoid blocking the cancellation even if the + # disconnect call fails. + with contextlib.suppress(Exception): + await self.disconnect() + raise + except Exception: + await self.disconnect() + raise + self._disconnected_event = asyncio.Event() return True @api_error_as_bleak_error async def disconnect(self) -> bool: """Disconnect from the peripheral device.""" - self._unsubscribe_connection_state() + self._async_disconnected_cleanup() await self._client.bluetooth_device_disconnect(self._address_as_int) await self._wait_for_free_connection_slot(DISCONNECT_TIMEOUT) return True @@ -322,9 +388,13 @@ class ESPHomeClient(BaseBleakClient): """ address_as_int = self._address_as_int entry_data = self.entry_data - if dangerous_use_bleak_cache and ( - cached_services := entry_data.get_gatt_services_cache(address_as_int) - ): + # If the connection version >= 3, we must use the cache + # because the esp has already wiped the services list to + # save memory. + if ( + self._connection_version >= MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE + or dangerous_use_bleak_cache + ) and (cached_services := entry_data.get_gatt_services_cache(address_as_int)): _LOGGER.debug( "%s: %s - %s: Cached services hit", self._source, @@ -370,6 +440,12 @@ class ESPHomeClient(BaseBleakClient): characteristic.handle, ) ) + + if not esphome_services.services: + # If we got no services, we must have disconnected + # or something went wrong on the ESP32's BLE stack. + raise BleakError("Failed to get services from remote esp") + self.services = services _LOGGER.debug( "%s: %s - %s: Cached services saved", @@ -392,6 +468,11 @@ class ESPHomeClient(BaseBleakClient): raise BleakError(f"Characteristic {char_specifier} was not found!") return characteristic + async def clear_cache(self) -> None: + """Clear the GATT cache.""" + self.entry_data.clear_gatt_services_cache(self._address_as_int) + self.entry_data.clear_gatt_mtu_cache(self._address_as_int) + @verify_connected @api_error_as_bleak_error async def read_gatt_char( @@ -494,12 +575,52 @@ class ESPHomeClient(BaseBleakClient): f"characteristic:{characteristic.uuid} " f"handle:{ble_handle}" ) - cancel_coro = await self._client.bluetooth_gatt_start_notify( + if ( + "notify" not in characteristic.properties + and "indicate" not in characteristic.properties + ): + raise BleakError( + f"Characteristic {characteristic.uuid} does not have notify or indicate property set." + ) + + self._notify_cancels[ + ble_handle + ] = await self._client.bluetooth_gatt_start_notify( self._address_as_int, ble_handle, lambda handle, data: callback(data), ) - self._notify_cancels[ble_handle] = cancel_coro + + if self._connection_version < MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE: + return + + # For connection v3 we are responsible for enabling notifications + # on the cccd (characteristic client config descriptor) handle since + # the esp32 will not have resolved the characteristic descriptors to + # save memory since doing so can exhaust the memory and cause a soft + # reset + cccd_descriptor = characteristic.get_descriptor(CCCD_UUID) + if not cccd_descriptor: + raise BleakError( + f"Characteristic {characteristic.uuid} does not have a " + "characteristic client config descriptor." + ) + + _LOGGER.debug( + "%s: %s - %s: Writing to CCD descriptor %s for notifications with properties=%s", + self._source, + self._ble_device.name, + self._ble_device.address, + cccd_descriptor.handle, + characteristic.properties, + ) + supports_notify = "notify" in characteristic.properties + await self._client.bluetooth_gatt_write_descriptor( + self._address_as_int, + cccd_descriptor.handle, + CCCD_NOTIFY_BYTES if supports_notify else CCCD_INDICATE_BYTES, + wait_for_response=False, + ) @api_error_as_bleak_error async def stop_notify( @@ -516,5 +637,18 @@ class ESPHomeClient(BaseBleakClient): characteristic = self._resolve_characteristic(char_specifier) # Do not raise KeyError if notifications are not enabled on this characteristic # to be consistent with the behavior of the BlueZ backend - if coro := self._notify_cancels.pop(characteristic.handle, None): - await coro() + if notify_cancel := self._notify_cancels.pop(characteristic.handle, None): + notify_stop, _ = notify_cancel + await notify_stop() + + def __del__(self) -> None: + """Destructor to make sure the connection state is unsubscribed.""" + if self._cancel_connection_state: + _LOGGER.warning( + "%s: %s - %s: ESPHomeClient bleak client was not properly disconnected before destruction", + self._source, + self._ble_device.name, + self._ble_device.address, + ) + if not self._hass.loop.is_closed(): + self._hass.loop.call_soon_threadsafe(self._async_disconnected_cleanup) diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 4fbaf7cabb6..ea44ff45d1c 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -1,147 +1,45 @@ """Bluetooth scanner for esphome.""" from __future__ import annotations -from collections.abc import Callable -import datetime -from datetime import timedelta -import re -import time -from typing import Final +from typing import Any from aioesphomeapi import BluetoothLEAdvertisement -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData -from homeassistant.components.bluetooth import ( - FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, - BaseHaScanner, - BluetoothServiceInfoBleak, - HaBluetoothConnector, -) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util.dt import monotonic_time_coarse - -TWO_CHAR = re.compile("..") - -# The maximum time between advertisements for a device to be considered -# stale when the advertisement tracker can determine the interval for -# connectable devices. -# -# BlueZ uses 180 seconds by default but we give it a bit more time -# to account for the esp32's bluetooth stack being a bit slower -# than BlueZ's. -CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 195 +from homeassistant.components.bluetooth import BaseHaRemoteScanner +from homeassistant.core import callback -class ESPHomeScanner(BaseHaScanner): +class ESPHomeScanner(BaseHaRemoteScanner): """Scanner for esphome.""" - def __init__( - self, - hass: HomeAssistant, - scanner_id: str, - new_info_callback: Callable[[BluetoothServiceInfoBleak], None], - connector: HaBluetoothConnector, - connectable: bool, - ) -> None: - """Initialize the scanner.""" - super().__init__(hass, scanner_id) - self._new_info_callback = new_info_callback - self._discovered_device_advertisement_datas: dict[ - str, tuple[BLEDevice, AdvertisementData] - ] = {} - self._discovered_device_timestamps: dict[str, float] = {} - self._connector = connector - self._connectable = connectable - self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} - self._fallback_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS - if connectable: - self._details["connector"] = connector - self._fallback_seconds = ( - CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS - ) - - @callback - def async_setup(self) -> CALLBACK_TYPE: - """Set up the scanner.""" - return async_track_time_interval( - self.hass, self._async_expire_devices, timedelta(seconds=30) - ) - - def _async_expire_devices(self, _datetime: datetime.datetime) -> None: - """Expire old devices.""" - now = time.monotonic() - expired = [ - address - for address, timestamp in self._discovered_device_timestamps.items() - if now - timestamp > self._fallback_seconds - ] - for address in expired: - del self._discovered_device_advertisement_datas[address] - del self._discovered_device_timestamps[address] - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return a list of discovered devices.""" - return [ - device_advertisement_data[0] - for device_advertisement_data in self._discovered_device_advertisement_datas.values() - ] - - @property - def discovered_devices_and_advertisement_data( - self, - ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: - """Return a list of discovered devices and advertisement data.""" - return self._discovered_device_advertisement_datas - @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" - now = monotonic_time_coarse() - address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper - name = adv.name - if prev_discovery := self._discovered_device_advertisement_datas.get(address): - # If the last discovery had the full local name - # and this one doesn't, keep the old one as we - # always want the full local name over the short one - prev_device = prev_discovery[0] - if len(prev_device.name) > len(adv.name): - name = prev_device.name + # The mac address is a uint64, but we need a string + mac_hex = f"{adv.address:012X}" + self._async_on_advertisement( + f"{mac_hex[0:2]}:{mac_hex[2:4]}:{mac_hex[4:6]}:{mac_hex[6:8]}:{mac_hex[8:10]}:{mac_hex[10:12]}", + adv.rssi, + adv.name, + adv.service_uuids, + adv.service_data, + adv.manufacturer_data, + None, + {"address_type": adv.address_type}, + ) - advertisement_data = AdvertisementData( - local_name=None if name == "" else name, - manufacturer_data=adv.manufacturer_data, - service_data=adv.service_data, - service_uuids=adv.service_uuids, - rssi=adv.rssi, - tx_power=-127, - platform_data=(), - ) - device = BLEDevice( # type: ignore[no-untyped-call] - address=address, - name=name, - details=self._details, - rssi=adv.rssi, # deprecated, will be removed in newer bleak - ) - self._discovered_device_advertisement_datas[address] = ( - device, - advertisement_data, - ) - self._discovered_device_timestamps[address] = now - self._new_info_callback( - BluetoothServiceInfoBleak( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=adv.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=self.source, - device=device, - advertisement=advertisement_data, - connectable=self._connectable, - time=now, - ) - ) + async def async_diagnostics(self) -> dict[str, Any]: + """Return diagnostic information about the scanner.""" + return await super().async_diagnostics() | { + "type": self.__class__.__name__, + "discovered_devices_and_advertisement_data": [ + { + "name": device_adv[0].name, + "address": device_adv[0].address, + "rssi": device_adv[0].rssi, + "advertisement_data": device_adv[1], + "details": device_adv[0].details, + } + for device_adv in self.discovered_devices_and_advertisement_data.values() + ], + } diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 4f38d1caa24..352068aaa57 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -197,9 +197,9 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti return self._static_info.visual_max_temperature @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" - features = 0 + features = ClimateEntityFeature(0) if self._static_info.supports_two_point_target_temperature: features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE else: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ea64fb7fb7f..542aa011a7b 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -17,7 +17,7 @@ from aioesphomeapi import ( import voluptuous as vol from homeassistant.components import dhcp, zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -40,6 +40,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._password: str | None = None self._noise_psk: str | None = None self._device_info: DeviceInfo | None = None + self._reauth_entry: ConfigEntry | None = None async def _async_step_user_base( self, user_input: dict[str, Any] | None = None, error: str | None = None @@ -72,6 +73,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by a reauth event.""" entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None + self._reauth_entry = entry self._host = entry.data[CONF_HOST] self._port = entry.data[CONF_PORT] self._password = entry.data[CONF_PASSWORD] @@ -245,10 +247,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PASSWORD: self._password or "", CONF_NOISE_PSK: self._noise_psk or "", } - if "entry_id" in self.context: - entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) - assert entry is not None - self.hass.config_entries.async_update_entry(entry, data=config_data) + if self._reauth_entry: + entry = self._reauth_entry + self.hass.config_entries.async_update_entry( + entry, data=self._reauth_entry.data | config_data + ) # Reload the config entry to notify of updated config self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) @@ -332,7 +335,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = self._device_info.name await self.async_set_unique_id(self._name, raise_on_progress=False) - self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) + if not self._reauth_entry: + self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) return None diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 10662977307..1b3e42d5a17 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -38,7 +38,7 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """A cover implementation for ESPHome.""" @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" flags = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index cc82fd536e7..68f195a23fb 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, cast +from homeassistant.components.bluetooth import async_scanner_by_source from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD @@ -29,4 +30,13 @@ async def async_get_config_entry_diagnostics( storage_data = cast("dict[str, Any]", storage_data) diag["storage_data"] = storage_data + if config_entry.unique_id and ( + scanner := async_scanner_by_source(hass, config_entry.unique_id) + ): + diag["bluetooth"] = { + "connections_free": entry_data.ble_connections_free, + "connections_limit": entry_data.ble_connections_limit, + "scanner": await scanner.async_diagnostics(), + } + return async_redact_data(diag, REDACT_KEYS) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index faa9074b880..2e05e01309e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -98,6 +98,9 @@ class RuntimeEntryData: _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] ) + _gatt_mtu_cache: MutableMapping[int, int] = field( + default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] + ) @property def name(self) -> str: @@ -116,6 +119,22 @@ class RuntimeEntryData: """Set the BleakGATTServiceCollection for the given address.""" self._gatt_services_cache[address] = services + def clear_gatt_services_cache(self, address: int) -> None: + """Clear the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache.pop(address, None) + + def get_gatt_mtu_cache(self, address: int) -> int | None: + """Get the mtu cache for the given address.""" + return self._gatt_mtu_cache.get(address) + + def set_gatt_mtu_cache(self, address: int, mtu: int) -> None: + """Set the mtu cache for the given address.""" + self._gatt_mtu_cache[address] = mtu + + def clear_gatt_mtu_cache(self, address: int) -> None: + """Clear the mtu cache for the given address.""" + self._gatt_mtu_cache.pop(address, None) + @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 772a1b8befa..27952d36c60 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -158,9 +158,9 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): return _FAN_DIRECTIONS.from_esphome(self._state.direction) @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - flags = 0 + flags = FanEntityFeature(0) if self._static_info.supports_oscillation: flags |= FanEntityFeature.OSCILLATE if self._static_info.supports_speed: diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 624dfc8950f..76de857a863 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -347,9 +347,9 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return self._static_info.supported_color_modes_compat(self._api_version) @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag supported features.""" - flags: int = LightEntityFeature.FLASH + flags = LightEntityFeature.FLASH # All color modes except UNKNOWN,ON_OFF support transition modes = self._native_supported_color_modes diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index bcfa0131518..947ea4729bb 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -38,9 +38,11 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): return self._static_info.assumed_state @property - def supported_features(self) -> int: + def supported_features(self) -> LockEntityFeature: """Flag supported features.""" - return LockEntityFeature.OPEN if self._static_info.supports_open else 0 + if self._static_info.supports_open: + return LockEntityFeature.OPEN + return LockEntityFeature(0) @property def code_format(self) -> str | None: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 2070b8ae362..ae11aa59fce 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==11.4.3"], + "requirements": ["aioesphomeapi==13.0.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index d7a70737690..7f90f4e27d8 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -83,7 +83,7 @@ class EsphomeMediaPlayer( return self._state.volume @property - def supported_features(self) -> int: + def supported_features(self) -> MediaPlayerEntityFeature: """Flag supported features.""" flags = ( MediaPlayerEntityFeature.PLAY_MEDIA diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index a00d4456227..4111a616439 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -1,11 +1,12 @@ """Support for esphome numbers.""" from __future__ import annotations +from contextlib import suppress import math from aioesphomeapi import NumberInfo, NumberMode as EsphomeNumberMode, NumberState -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,6 +48,13 @@ NUMBER_MODES: EsphomeEnumMapper[EsphomeNumberMode, NumberMode] = EsphomeEnumMapp class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """A number implementation for esphome.""" + @property + def device_class(self) -> NumberDeviceClass | None: + """Return the class of this entity.""" + with suppress(ValueError): + return NumberDeviceClass(self._static_info.device_class) + return None + @property def native_min_value(self) -> float: """Return the minimum value.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 4b316f6a640..29c661f0984 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -1,6 +1,7 @@ """Support for esphome sensors.""" from __future__ import annotations +from contextlib import suppress from datetime import datetime import math @@ -14,7 +15,6 @@ from aioesphomeapi import ( from aioesphomeapi.model import LastResetType from homeassistant.components.sensor import ( - DEVICE_CLASSES, SensorDeviceClass, SensorEntity, SensorStateClass, @@ -96,11 +96,11 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): return self._static_info.unit_of_measurement @property - def device_class(self) -> str | None: + def device_class(self) -> SensorDeviceClass | None: """Return the class of this device, from component DEVICE_CLASSES.""" - if self._static_info.device_class not in DEVICE_CLASSES: - return None - return self._static_info.device_class + with suppress(ValueError): + return SensorDeviceClass(self._static_info.device_class) + return None @property def state_class(self) -> SensorStateClass | None: diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index b1b1ba94e3f..0ec4d93b405 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -43,5 +43,11 @@ } }, "flow_title": "{name}" + }, + "issues": { + "ble_firmware_outdated": { + "title": "Update {name} with ESPHome 2022.11.0 or later", + "description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome 2022.11.0 or later." + } } } diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index db5084df378..3888053d3fb 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -1,11 +1,12 @@ """Support for ESPHome switches.""" from __future__ import annotations +from contextlib import suppress from typing import Any from aioesphomeapi import SwitchInfo, SwitchState -from homeassistant.components.switch import DEVICE_CLASSES, SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -43,11 +44,11 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): return self._state.state @property - def device_class(self) -> str | None: + def device_class(self) -> SwitchDeviceClass | None: """Return the class of this device.""" - if self._static_info.device_class not in DEVICE_CLASSES: - return None - return self._static_info.device_class + with suppress(ValueError): + return SwitchDeviceClass(self._static_info.device_class) + return None async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" diff --git a/homeassistant/components/esphome/translations/bg.json b/homeassistant/components/esphome/translations/bg.json index 699a993403f..553ca45a10f 100644 --- a/homeassistant/components/esphome/translations/bg.json +++ b/homeassistant/components/esphome/translations/bg.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "ESP \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 ESP. \u041c\u043e\u043b\u044f, \u0443\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 YAML \u0444\u0430\u0439\u043b \u0441\u044a\u0434\u044a\u0440\u0436\u0430 \u0440\u0435\u0434 \"api:\".", - "resolve_error": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043e\u0442\u043a\u0440\u0438\u0435 \u0430\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0430 ESP. \u0410\u043a\u043e \u0442\u0430\u0437\u0438 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0430\u0432\u0430, \u0437\u0430\u0434\u0430\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043e\u0442\u043a\u0440\u0438\u0435 \u0430\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0430 ESP. \u0410\u043a\u043e \u0442\u0430\u0437\u0438 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0430\u0432\u0430, \u0437\u0430\u0434\u0430\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441." }, "flow_title": "ESPHome: {name}", "step": { @@ -39,5 +39,11 @@ "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 [ESPHome](https://esphomelib.com/)." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "\u0417\u0430 \u0434\u0430 \u043f\u043e\u0434\u043e\u0431\u0440\u0438\u0442\u0435 \u043d\u0430\u0434\u0435\u0436\u0434\u043d\u043e\u0441\u0442\u0442\u0430 \u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 Bluetooth, \u0441\u0438\u043b\u043d\u043e \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 \u0434\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0442\u0435 {name} \u0441 ESPHome 2022.11.0 \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044f.", + "title": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 {name} \u0441 ESPHome 2022.11.0 \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044f" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index ef9814d3ac8..ce4b9ce5da2 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -43,5 +43,11 @@ "description": "Introdueix la configuraci\u00f3 de connexi\u00f3 del node [ESPHome]({esphome_url})." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Per millorar la fiabilitat i el rendiment de Bluetooth, et recomanem que actualitzis {name} a ESPHome 2022.11.0 o posterior.", + "title": "Actualitza {name} amb ESPHome 2022.11.0 o posterior" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/cs.json b/homeassistant/components/esphome/translations/cs.json index cdd86bd2577..9b4976ddaa1 100644 --- a/homeassistant/components/esphome/translations/cs.json +++ b/homeassistant/components/esphome/translations/cs.json @@ -43,5 +43,11 @@ "description": "Zadejte pros\u00edm nastaven\u00ed p\u0159ipojen\u00ed va\u0161eho [ESPHome](https://esphomelib.com/) uzlu." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Chcete-li zlep\u0161it spolehlivost a v\u00fdkon Bluetooth, d\u016frazn\u011b doporu\u010dujeme aktualizovat {name} na ESPHome 2022.11.0 nebo nov\u011bj\u0161\u00ed.", + "title": "Aktualizovat {name} pomoc\u00ed ESPHome 2022.11.0 nebo nov\u011bj\u0161\u00ed" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 7556739ce93..bf7bcd386e5 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -43,5 +43,11 @@ "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome]( {esphome_url} )-Knotens ein." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Um die Zuverl\u00e4ssigkeit und Leistung von Bluetooth zu verbessern, empfehlen wir dringend, {name} mit ESPHome 2022.11.0 oder h\u00f6her zu aktualisieren.", + "title": "Aktualisieren von {name} mit ESPHome 2022.11.0 oder h\u00f6her" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/el.json b/homeassistant/components/esphome/translations/el.json index 405b1a55a89..041628a3673 100644 --- a/homeassistant/components/esphome/translations/el.json +++ b/homeassistant/components/esphome/translations/el.json @@ -43,5 +43,11 @@ "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 [ESPHome](https://esphomelib.com/)." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03be\u03b9\u03bf\u03c0\u03b9\u03c3\u03c4\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Bluetooth, \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c6\u03cd\u03bb\u03b1\u03ba\u03c4\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {name} \u03bc\u03b5 ESPHome 2022.11.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 {name} \u03bc\u03b5 ESPHome 2022.11.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index b0b502631df..173113f64cd 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -43,5 +43,11 @@ "description": "Please enter connection settings of your [ESPHome]({esphome_url}) node." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome 2022.11.0 or later.", + "title": "Update {name} with ESPHome 2022.11.0 or later" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 82066953472..2b10be769e4 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -43,5 +43,11 @@ "description": "Por favor, introduce la configuraci\u00f3n de conexi\u00f3n de tu nodo [ESPHome]({esphome_url})." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Para mejorar la confiabilidad y el rendimiento de Bluetooth, recomendamos actualizar {name} con ESPHome 2022.11.0 o posterior.", + "title": "Actualizar {name} con ESPHome 2022.11.0 o posterior" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index 29fa98bafb0..07ec62741f9 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -43,5 +43,11 @@ "description": "Sisesta oma [ESPHome]({esphome_url}) s\u00f5lme \u00fchenduse s\u00e4tted." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Bluetoothi t\u00f6\u00f6kindluse ja j\u00f5udluse parandamiseks soovitame tungivalt v\u00e4rskendada {name} versiooniga ESPHome 2022.11.0 v\u00f5i uuema versiooniga.", + "title": "Uuenda {nimi} ESPHome 2022.11.0 v\u00f5i uuema versiooniga" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 8ac9feb8a93..ab7e9545931 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -43,5 +43,10 @@ "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome]({esphome_url})." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Pour am\u00e9liorer la fiabilit\u00e9 et les performances du Bluetooth, nous recommandons vivement de mettre \u00e0 jour {nom} avec ESPHome 2022.11.0 ou ult\u00e9rieure." + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hr.json b/homeassistant/components/esphome/translations/hr.json new file mode 100644 index 00000000000..5984e832e28 --- /dev/null +++ b/homeassistant/components/esphome/translations/hr.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "authenticate": { + "data": { + "password": "Lozinka" + } + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 41550d02a43..5fd2dfc2565 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -43,5 +43,11 @@ "description": "K\u00e9rem, adja meg az [ESPHome]({esphome_url}) v\u00e9gpontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "A Bluetooth megb\u00edzhat\u00f3s\u00e1g\u00e1nak \u00e9s teljes\u00edtm\u00e9ny\u00e9nek jav\u00edt\u00e1sa \u00e9rdek\u00e9ben javasoljuk {name} v\u00e9gpont friss\u00edt\u00e9s\u00e9t az ESPHome 2022.11.0 vagy \u00fajabb verzi\u00f3ra.", + "title": "{name} v\u00e9gpont friss\u00edt\u00e9se ESPHome 2022.11.0 vagy \u00fajabb verzi\u00f3j\u00e1val" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index 155bf33d986..8a82425a924 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -43,5 +43,11 @@ "description": "Masukkan pengaturan koneksi node [ESPHome]({esphome_url})." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Untuk meningkatkan keandalan dan performa Bluetooth, kami sangat menyarankan untuk memperbarui {name} dengan ESPHome 2022.11.0 atau yang lebih baru.", + "title": "Perbarui {name} dengan ESPHome 2022.11.0 atau yang lebih baru" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index b1c4e12d088..0f0b31f7094 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -21,7 +21,7 @@ }, "discovery_confirm": { "description": "Vuoi aggiungere il nodo ESPHome `{name}` a Home Assistant?", - "title": "Trovato nodo ESPHome" + "title": "Rilevato nodo ESPHome" }, "encryption_key": { "data": { @@ -43,5 +43,11 @@ "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome]({esphome_url})." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Per migliorare l'affidabilit\u00e0 e le prestazioni Bluetooth, si consiglia vivamente di aggiornare {name} con ESPHome 2022.11.0 o versioni successive.", + "title": "Aggiorna {name} con ESPHome 2022.11.0 o versioni successive" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index aae5b639eb8..410850916ea 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -43,5 +43,11 @@ "description": "Voer de verbindingsinstellingen van je [ESPHome](https://esphome.io/) apparaat in." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Om de Bluetooth betrouwbaarheid en performance te verbeteren, raden we sterk aan om {name} bij te werken met ESPHome 2022.11.0 of later.", + "title": "Werk {name} bij naar ESPHome versie 2022.11.0 of later" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index e17b9dad815..d24d557f49e 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -43,5 +43,11 @@ "description": "Vennligst skriv inn tilkoblingsinnstillingene for [ESPHome]( {esphome_url} )-noden." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "For \u00e5 forbedre Bluetooth-p\u00e5litelighet og ytelse anbefaler vi p\u00e5 det sterkeste \u00e5 oppdatere {name} med ESPHome 2022.11.0 eller nyere.", + "title": "Oppdater {name} med ESPHome 2022.11.0 eller nyere" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index dd495f0f097..ae9ae8c7b18 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -43,5 +43,11 @@ "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia w\u0119z\u0142a [ESPHome]({esphome_url})." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Aby poprawi\u0107 niezawodno\u015b\u0107 i wydajno\u015b\u0107 Bluetooth, zdecydowanie zalecamy zaktualizowanie {name} do wersji ESPHome 2022.11.0 lub nowszej.", + "title": "Zaktualizuj {name} do wersji ESPHome 2022.11.0 lub nowszej" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index 21fd9067be1..ae78ce6b88c 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -43,5 +43,11 @@ "description": "Insira as configura\u00e7\u00f5es de conex\u00e3o do seu n\u00f3 [ESPHome]( {esphome_url} )." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Para melhorar a confiabilidade e o desempenho do Bluetooth, recomendamos atualizar {name} com ESPHome 2022.11.0 ou posterior.", + "title": "Atualize {name} com ESPHome 2022.11.0 ou posterior" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index ea0a3105226..7aa09cf7ff4 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -43,5 +43,11 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 [ESPHome]({esphome_url})." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "\u0414\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u043d\u0430\u0434\u0435\u0436\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 Bluetooth \u043c\u044b \u043d\u0430\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c {name} \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 ESPHome 2022.11.0 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u0435\u0439.", + "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ESPHome {name} \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 2022.11.0 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u0435\u0439" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/sk.json b/homeassistant/components/esphome/translations/sk.json index ca12462f388..f87d3e7e940 100644 --- a/homeassistant/components/esphome/translations/sk.json +++ b/homeassistant/components/esphome/translations/sk.json @@ -37,10 +37,17 @@ }, "user": { "data": { + "host": "Hostite\u013e", "port": "Port" }, "description": "Pros\u00edm, zadajte nastavenia pripojenia v\u00e1\u0161ho uzla [ESPHome](https://esphomelib.com/)." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Ak chcete zlep\u0161i\u0165 spo\u013eahlivos\u0165 a v\u00fdkon Bluetooth, d\u00f4razne odpor\u00fa\u010dame aktualizova\u0165 {name} na ESPHome 2022.11.0 alebo nov\u0161\u00ed.", + "title": "Aktualizujte {name} pomocou ESPHome 2022.11.0 alebo nov\u0161ieho" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 44f50a433e3..b08d5d97c34 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -43,5 +43,11 @@ "description": "\u8acb\u8f38\u5165 [ESPHome]({esphome_url}) \u7bc0\u9ede\u9023\u7dda\u8cc7\u8a0a\u3002" } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "\u6b32\u6539\u5584\u85cd\u82bd\u53ef\u9760\u6027\u8207\u6548\u80fd\uff0c\u5f37\u70c8\u5efa\u8b70\u60a8\u66f4\u65b0\u81f3 2022.11.0 \u7248\u6216\u66f4\u65b0\u7248\u672c ESPHome \u4e4b {name}\u3002", + "title": "\u66f4\u65b0\u81f3 2022.11.0 \u7248\u6216\u66f4\u65b0\u7248\u672c ESPHome \u4e4b {name} " + } } } \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/sk.json b/homeassistant/components/evil_genius_labs/translations/sk.json new file mode 100644 index 00000000000..7041756cda4 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "timeout": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 0ec64c6b2b1..4594268623c 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -321,9 +321,8 @@ class EvoController(EvoClimateEntity): self._attr_preset_modes = [ TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA) ] - self._attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE if self._attr_preset_modes else 0 - ) + if self._attr_preset_modes: + self._attr_supported_features = ClimateEntityFeature.PRESET_MODE async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller. diff --git a/homeassistant/components/ezviz/translations/ru.json b/homeassistant/components/ezviz/translations/ru.json index 13bdf601817..77c3332d0bc 100644 --- a/homeassistant/components/ezviz/translations/ru.json +++ b/homeassistant/components/ezviz/translations/ru.json @@ -8,7 +8,7 @@ "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{serial}", "step": { diff --git a/homeassistant/components/ezviz/translations/sk.json b/homeassistant/components/ezviz/translations/sk.json index 5ada995aa6e..15dbad718a4 100644 --- a/homeassistant/components/ezviz/translations/sk.json +++ b/homeassistant/components/ezviz/translations/sk.json @@ -1,7 +1,39 @@ { "config": { + "abort": { + "already_configured_account": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripojte sa k EZVIZ Cloud" + }, + "user_custom_url": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Ru\u010dne zadajte adresu URL v\u00e1\u0161ho regi\u00f3nu", + "title": "Pripojenie k vlastnej adrese URL EZVIZ" + } } } } \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/sk.json b/homeassistant/components/faa_delays/translations/sk.json index bbadecd391b..bbea900554a 100644 --- a/homeassistant/components/faa_delays/translations/sk.json +++ b/homeassistant/components/faa_delays/translations/sk.json @@ -1,7 +1,9 @@ { "config": { "error": { - "invalid_airport": "K\u00f3d letiska je neplatn\u00fd" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_airport": "K\u00f3d letiska je neplatn\u00fd", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" } } } \ No newline at end of file diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index d44335fad07..e143b93258e 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -from enum import IntEnum +from enum import IntFlag import functools as ft import logging import math @@ -41,7 +41,7 @@ SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" -class FanEntityFeature(IntEnum): +class FanEntityFeature(IntFlag): """Supported features of the fan entity.""" SET_SPEED = 1 @@ -190,7 +190,7 @@ class FanEntity(ToggleEntity): _attr_preset_mode: str | None _attr_preset_modes: list[str] | None _attr_speed_count: int - _attr_supported_features: int = 0 + _attr_supported_features: FanEntityFeature = FanEntityFeature(0) def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" @@ -363,7 +363,7 @@ class FanEntity(ToggleEntity): return data @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" return self._attr_supported_features diff --git a/homeassistant/components/fan/translations/is.json b/homeassistant/components/fan/translations/is.json index f06d44366b0..840591cb4e3 100644 --- a/homeassistant/components/fan/translations/is.json +++ b/homeassistant/components/fan/translations/is.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} er sl\u00f6kkt", + "is_on": "{entity_name} er \u00ed gangi" + } + }, "state": { "_": { "off": "Sl\u00f6kkt", diff --git a/homeassistant/components/fan/translations/sk.json b/homeassistant/components/fan/translations/sk.json index 1dc17560e34..9251981601d 100644 --- a/homeassistant/components/fan/translations/sk.json +++ b/homeassistant/components/fan/translations/sk.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "turn_off": "Vypn\u00fa\u0165 {entity_name}", + "turn_on": "Zapn\u00fa\u0165 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} je vypnut\u00e9", + "is_on": "{entity_name} je zapnut\u00e9" + }, + "trigger_type": { + "changed_states": "{entity_name} zapnut\u00e9 alebo vypnut\u00e9", + "turned_off": "{entity_name} vypnut\u00e1", + "turned_on": "{entity_name} zapnut\u00e1" + } + }, "state": { "_": { "off": "Neakt\u00edvny", diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 19f36742740..6bf86be7296 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -79,6 +79,7 @@ FIBARO_TYPEMAP = { "com.fibaro.colorController": Platform.LIGHT, "com.fibaro.securitySensor": Platform.BINARY_SENSOR, "com.fibaro.hvac": Platform.CLIMATE, + "com.fibaro.hvacSystem": Platform.CLIMATE, "com.fibaro.setpoint": Platform.CLIMATE, "com.fibaro.FGT001": Platform.CLIMATE, "com.fibaro.thermostatDanfoss": Platform.CLIMATE, diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index f9baa33c41f..7954334689d 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import json -from typing import Any +from typing import Any, cast from homeassistant.components.binary_sensor import ( ENTITY_ID_FORMAT, @@ -69,7 +69,9 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): elif fibaro_device.baseType in SENSOR_TYPES: self._fibaro_sensor_type = fibaro_device.baseType if self._fibaro_sensor_type: - self._attr_device_class = SENSOR_TYPES[self._fibaro_sensor_type][2] + self._attr_device_class = cast( + BinarySensorDeviceClass, SENSOR_TYPES[self._fibaro_sensor_type][2] + ) self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1] @property diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 90a13fe8988..7731b2544c4 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -1,6 +1,7 @@ """Support for Fibaro thermostats.""" from __future__ import annotations +from contextlib import suppress import logging from typing import Any @@ -10,6 +11,7 @@ from homeassistant.components.climate import ( PRESET_BOOST, ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.config_entries import ConfigEntry @@ -98,6 +100,14 @@ HA_OPMODES_HVAC = { HVACMode.FAN_ONLY: 6, } +TARGET_TEMP_ACTIONS = ( + "setTargetLevel", + "setThermostatSetpoint", + "setHeatingThermostatSetpoint", +) + +OP_MODE_ACTIONS = ("setMode", "setOperatingMode", "setThermostatMode") + async def async_setup_entry( hass: HomeAssistant, @@ -126,7 +136,6 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._target_temp_device: FibaroDevice | None = None self._op_mode_device: FibaroDevice | None = None self._fan_mode_device: FibaroDevice | None = None - self._attr_supported_features = 0 self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) siblings = fibaro_device.fibaro_controller.get_siblings(fibaro_device) @@ -150,16 +159,14 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._temp_sensor_device = FibaroDevice(device) tempunit = device.properties.unit - if ( - "setTargetLevel" in device.actions - or "setThermostatSetpoint" in device.actions - or "setHeatingThermostatSetpoint" in device.actions + if any( + action for action in TARGET_TEMP_ACTIONS if action in device.actions ): self._target_temp_device = FibaroDevice(device) self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE tempunit = device.properties.unit - if "setMode" in device.actions or "setOperatingMode" in device.actions: + if any(action for action in OP_MODE_ACTIONS if action in device.actions): self._op_mode_device = FibaroDevice(device) self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE @@ -189,18 +196,27 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._attr_preset_modes = [] self._attr_hvac_modes = [] prop = self._op_mode_device.fibaro_device.properties - if "supportedOperatingModes" in prop: - op_modes = prop.supportedOperatingModes.split(",") - elif "supportedModes" in prop: - op_modes = prop.supportedModes.split(",") - for mode in op_modes: - mode = int(mode) - if mode in OPMODES_HVAC: - mode_ha = OPMODES_HVAC[mode] - if mode_ha not in self._attr_hvac_modes: + if "supportedThermostatModes" in prop: + for mode in prop.supportedThermostatModes: + try: + self._attr_hvac_modes.append(HVACMode(mode.lower())) + except ValueError: + self._attr_preset_modes.append(mode) + else: + if "supportedOperatingModes" in prop: + op_modes = prop.supportedOperatingModes.split(",") + else: + op_modes = prop.supportedModes.split(",") + for mode in op_modes: + mode = int(mode) + if ( + mode in OPMODES_HVAC + and (mode_ha := OPMODES_HVAC.get(mode)) + and mode_ha not in self._attr_hvac_modes + ): self._attr_hvac_modes.append(mode_ha) - if mode in OPMODES_PRESET: - self._attr_preset_modes.append(OPMODES_PRESET[mode]) + if mode in OPMODES_PRESET: + self._attr_preset_modes.append(OPMODES_PRESET[mode]) async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" @@ -239,20 +255,30 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._fan_mode_device.action("setFanMode", HA_FANMODES[fan_mode]) @property - def fibaro_op_mode(self) -> int: + def fibaro_op_mode(self) -> str | int: """Return the operating mode of the device.""" if not self._op_mode_device: - return 3 # Default to AUTO + return HA_OPMODES_HVAC[HVACMode.AUTO] - if "operatingMode" in self._op_mode_device.fibaro_device.properties: - return int(self._op_mode_device.fibaro_device.properties.operatingMode) + prop = self._op_mode_device.fibaro_device.properties - return int(self._op_mode_device.fibaro_device.properties.mode) + if "operatingMode" in prop: + return int(prop.operatingMode) + if "thermostatMode" in prop: + return prop.thermostatMode + + return int(prop.mode) @property - def hvac_mode(self) -> HVACMode: - """Return current operation ie. heat, cool, idle.""" - return OPMODES_HVAC[self.fibaro_op_mode] + def hvac_mode(self) -> HVACMode | str | None: + """Return hvac operation ie. heat, cool, idle.""" + fibaro_operation_mode = self.fibaro_op_mode + if isinstance(fibaro_operation_mode, str): + with suppress(ValueError): + return HVACMode(fibaro_operation_mode.lower()) + elif fibaro_operation_mode in OPMODES_HVAC: + return OPMODES_HVAC[fibaro_operation_mode] + return None def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" @@ -263,9 +289,29 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): if "setOperatingMode" in self._op_mode_device.fibaro_device.actions: self._op_mode_device.action("setOperatingMode", HA_OPMODES_HVAC[hvac_mode]) + elif "setThermostatMode" in self._op_mode_device.fibaro_device.actions: + prop = self._op_mode_device.fibaro_device.properties + if "supportedThermostatModes" in prop: + for mode in prop.supportedThermostatModes: + if mode.lower() == hvac_mode: + self._op_mode_device.action("setThermostatMode", mode) + break elif "setMode" in self._op_mode_device.fibaro_device.actions: self._op_mode_device.action("setMode", HA_OPMODES_HVAC[hvac_mode]) + @property + def hvac_action(self) -> HVACAction | None: + """Return the current running hvac operation if supported.""" + if not self._op_mode_device: + return None + + prop = self._op_mode_device.fibaro_device.properties + if "thermostatOperatingState" in prop: + with suppress(ValueError): + return HVACAction(prop.thermostatOperatingState.lower()) + + return None + @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. @@ -275,6 +321,11 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): if not self._op_mode_device: return None + if "thermostatMode" in self._op_mode_device.fibaro_device.properties: + mode = self._op_mode_device.fibaro_device.properties.thermostatMode + if self.preset_modes is not None and mode in self.preset_modes: + return mode + return None if "operatingMode" in self._op_mode_device.fibaro_device.properties: mode = int(self._op_mode_device.fibaro_device.properties.operatingMode) else: @@ -288,7 +339,10 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): """Set new preset mode.""" if self._op_mode_device is None: return - if "setOperatingMode" in self._op_mode_device.fibaro_device.actions: + + if "setThermostatMode" in self._op_mode_device.fibaro_device.actions: + self._op_mode_device.action("setThermostatMode", preset_mode) + elif "setOperatingMode" in self._op_mode_device.fibaro_device.actions: self._op_mode_device.action( "setOperatingMode", HA_OPMODES_PRESET[preset_mode] ) diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index 7a6d7422520..e5cb75890be 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -54,6 +54,18 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str } +def _normalize_url(url: str) -> str: + """Try to fix errors in the entered url. + + We know that the url should be in the format http:///api/ + """ + if url.endswith("/api"): + return f"{url}/" + if not url.endswith("/api/"): + return f"{url}api/" if url.endswith("/") else f"{url}/api/" + return url + + class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Fibaro.""" @@ -71,6 +83,7 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: + user_input[CONF_URL] = _normalize_url(user_input[CONF_URL]) info = await _validate_input(self.hass, user_input) except FibaroConnectFailed: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 218a6aad857..c7b80585548 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -1,6 +1,7 @@ { "domain": "fibaro", "name": "Fibaro", + "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": ["fiblary3==0.1.8"], "codeowners": ["@rappenze"], diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 797fc6d8d44..da1b66c6528 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -14,13 +14,12 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, - POWER_WATT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, Platform, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -67,7 +66,7 @@ MAIN_SENSOR_TYPES: dict[str, SensorEntityDescription] = { "com.fibaro.energyMeter": SensorEntityDescription( key="com.fibaro.energyMeter", name="Energy", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -79,14 +78,14 @@ ADDITIONAL_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="energy", name="Energy", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="power", name="Power", - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), @@ -94,8 +93,8 @@ ADDITIONAL_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( FIBARO_TO_HASS_UNIT: dict[str, str] = { "lux": LIGHT_LUX, - "C": TEMP_CELSIUS, - "F": TEMP_FAHRENHEIT, + "C": UnitOfTemperature.CELSIUS, + "F": UnitOfTemperature.FAHRENHEIT, } diff --git a/homeassistant/components/fibaro/translations/bg.json b/homeassistant/components/fibaro/translations/bg.json index 3eab800c93f..b03b7cff057 100644 --- a/homeassistant/components/fibaro/translations/bg.json +++ b/homeassistant/components/fibaro/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/fibaro/translations/sk.json b/homeassistant/components/fibaro/translations/sk.json index 649a1e186ee..33e7cce96e0 100644 --- a/homeassistant/components/fibaro/translations/sk.json +++ b/homeassistant/components/fibaro/translations/sk.json @@ -1,8 +1,26 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Aktualizujte svoje heslo pre {username}", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { + "password": "Heslo", + "url": "URL vo form\u00e1te http://HOST/api/", "username": "U\u017e\u00edvate\u013esk\u00e9 meno" } } diff --git a/homeassistant/components/file_upload/__init__.py b/homeassistant/components/file_upload/__init__.py index 9f548e14459..73f8465b1df 100644 --- a/homeassistant/components/file_upload/__init__.py +++ b/homeassistant/components/file_upload/__init__.py @@ -9,7 +9,8 @@ from pathlib import Path import shutil import tempfile -from aiohttp import web +from aiohttp import BodyPartReader, web +import janus import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -22,9 +23,8 @@ from homeassistant.util.ulid import ulid_hex DOMAIN = "file_upload" -# If increased, change upload view to streaming -# https://docs.aiohttp.org/en/stable/web_quickstart.html#file-uploads -MAX_SIZE = 1024 * 1024 * 10 +ONE_MEGABYTE = 1024 * 1024 +MAX_SIZE = 100 * ONE_MEGABYTE TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" @@ -126,14 +126,18 @@ class FileUploadView(HomeAssistantView): # Increase max payload request._client_max_size = MAX_SIZE # pylint: disable=protected-access - data = await request.post() - file_field = data.get("file") + reader = await request.multipart() + file_field_reader = await reader.next() - if not isinstance(file_field, web.FileField): + if ( + not isinstance(file_field_reader, BodyPartReader) + or file_field_reader.name != "file" + or file_field_reader.filename is None + ): raise vol.Invalid("Expected a file") try: - raise_if_invalid_filename(file_field.filename) + raise_if_invalid_filename(file_field_reader.filename) except ValueError as err: raise web.HTTPBadRequest from err @@ -145,19 +149,39 @@ class FileUploadView(HomeAssistantView): file_upload_data: FileUploadData = hass.data[DOMAIN] file_dir = file_upload_data.file_dir(file_id) + queue: janus.Queue[bytes | None] = janus.Queue() - def _sync_work() -> None: + def _sync_queue_consumer( + sync_q: janus.SyncQueue[bytes | None], _file_name: str + ) -> None: file_dir.mkdir() + with (file_dir / _file_name).open("wb") as file_handle: + while True: + _chunk = sync_q.get() + if _chunk is None: + break - # MyPy forgets about the isinstance check because we're in a function scope - assert isinstance(file_field, web.FileField) + file_handle.write(_chunk) + sync_q.task_done() - with (file_dir / file_field.filename).open("wb") as target_fileobj: - shutil.copyfileobj(file_field.file, target_fileobj) + fut: asyncio.Future[None] | None = None + try: + fut = hass.async_add_executor_job( + _sync_queue_consumer, + queue.sync_q, + file_field_reader.filename, + ) - await hass.async_add_executor_job(_sync_work) + while chunk := await file_field_reader.read_chunk(ONE_MEGABYTE): + queue.async_q.put_nowait(chunk) + if queue.async_q.qsize() > 5: # Allow up to 5 MB buffer size + await queue.async_q.join() + queue.async_q.put_nowait(None) # terminate queue consumer + finally: + if fut is not None: + await fut - file_upload_data.files[file_id] = file_field.filename + file_upload_data.files[file_id] = file_field_reader.filename return self.json({"file_id": file_id}) diff --git a/homeassistant/components/file_upload/manifest.json b/homeassistant/components/file_upload/manifest.json index d2b4f88a279..62f7a1f2b27 100644 --- a/homeassistant/components/file_upload/manifest.json +++ b/homeassistant/components/file_upload/manifest.json @@ -2,6 +2,7 @@ "domain": "file_upload", "name": "File Upload", "documentation": "https://www.home-assistant.io/integrations/file_upload", + "requirements": ["janus==1.0.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/filesize/translations/cs.json b/homeassistant/components/filesize/translations/cs.json index 1c38c1deb5d..ced67575b71 100644 --- a/homeassistant/components/filesize/translations/cs.json +++ b/homeassistant/components/filesize/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "not_allowed": "Cesta nen\u00ed povolena", "not_valid": "Cesta nen\u00ed platn\u00e1" } } diff --git a/homeassistant/components/filesize/translations/sk.json b/homeassistant/components/filesize/translations/sk.json new file mode 100644 index 00000000000..6c0218d4a8e --- /dev/null +++ b/homeassistant/components/filesize/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "not_allowed": "Cesta nie je povolen\u00e1", + "not_valid": "Cesta nie je platn\u00e1" + }, + "step": { + "user": { + "data": { + "file_path": "Cesta k s\u00faboru" + } + } + } + }, + "title": "Ve\u013ekos\u0165 s\u00faboru" +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index 3fcd3870f6a..d4d2b0763d9 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -1,9 +1,15 @@ """Config flow for FireServiceRota.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + from pyfireservicerota import FireServiceRota, InvalidAuthError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, URL_LIST @@ -110,18 +116,18 @@ class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=self._description_placeholders, ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Initialise re-authentication.""" + await self.async_set_unique_id(entry_data[CONF_USERNAME]) + self._existing_entry = {**entry_data} + self._description_placeholders = {CONF_USERNAME: entry_data[CONF_USERNAME]} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Get new tokens for a config entry that can't authenticate.""" - - if not self._existing_entry: - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._existing_entry = user_input.copy() - self._description_placeholders = {"username": user_input[CONF_USERNAME]} - user_input = None - if user_input is None: - return self._show_setup_form(step_id=config_entries.SOURCE_REAUTH) + return self._show_setup_form(step_id="reauth_confirm") - return await self._validate_and_create_entry( - user_input, config_entries.SOURCE_REAUTH - ) + return await self._validate_and_create_entry(user_input, "reauth_confirm") diff --git a/homeassistant/components/fireservicerota/strings.json b/homeassistant/components/fireservicerota/strings.json index aef6f1b6849..7c60b438264 100644 --- a/homeassistant/components/fireservicerota/strings.json +++ b/homeassistant/components/fireservicerota/strings.json @@ -8,7 +8,7 @@ "url": "Website" } }, - "reauth": { + "reauth_confirm": { "description": "Authentication tokens became invalid, login to recreate them.", "data": { "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/fireservicerota/translations/bg.json b/homeassistant/components/fireservicerota/translations/bg.json index 22cc783d4e9..738c6de5612 100644 --- a/homeassistant/components/fireservicerota/translations/bg.json +++ b/homeassistant/components/fireservicerota/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d" @@ -11,7 +11,7 @@ "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } diff --git a/homeassistant/components/fireservicerota/translations/ca.json b/homeassistant/components/fireservicerota/translations/ca.json index 261350db3f8..c46748da8bc 100644 --- a/homeassistant/components/fireservicerota/translations/ca.json +++ b/homeassistant/components/fireservicerota/translations/ca.json @@ -11,11 +11,11 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Contrasenya" }, - "description": "Els tokens d'autenticaci\u00f3 ja no s\u00f3n v\u00e0lids, inicia sessi\u00f3 per tornar-los a generar." + "description": "Els 'tokens' d'autenticaci\u00f3 ja no s\u00f3n v\u00e0lids, inicia sessi\u00f3 per tornar-los a generar." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/cs.json b/homeassistant/components/fireservicerota/translations/cs.json index 7ae758dab52..6edd5e1a056 100644 --- a/homeassistant/components/fireservicerota/translations/cs.json +++ b/homeassistant/components/fireservicerota/translations/cs.json @@ -11,11 +11,11 @@ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Heslo" }, - "description": "Ov\u011b\u0159ovac\u00ed tokeny jsou neplatn\u00e9. Chcete-li je znovu vytvo\u0159it, p\u0159ihlaste se." + "description": "Ov\u011b\u0159ovac\u00ed tokeny se staly neplatn\u00fdmi, p\u0159ihlaste se a vytvo\u0159te je znovu." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/de.json b/homeassistant/components/fireservicerota/translations/de.json index c8c18c4c372..8f571f27133 100644 --- a/homeassistant/components/fireservicerota/translations/de.json +++ b/homeassistant/components/fireservicerota/translations/de.json @@ -11,11 +11,11 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Passwort" }, - "description": "Authentifizierungs-Tokens sind ung\u00fcltig, melde dich an, um sie neu zu erstellen." + "description": "Die Authentifizierungs-Tokens wurden ung\u00fcltig, melde dich an, um sie neu zu erstellen." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/el.json b/homeassistant/components/fireservicerota/translations/el.json index 467a98a49ad..d78f43c6212 100644 --- a/homeassistant/components/fireservicerota/translations/el.json +++ b/homeassistant/components/fireservicerota/translations/el.json @@ -11,7 +11,7 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json index a059081760d..38762b614f4 100644 --- a/homeassistant/components/fireservicerota/translations/en.json +++ b/homeassistant/components/fireservicerota/translations/en.json @@ -11,7 +11,7 @@ "invalid_auth": "Invalid authentication" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Password" }, diff --git a/homeassistant/components/fireservicerota/translations/es-419.json b/homeassistant/components/fireservicerota/translations/es-419.json index cf14204ec0c..62f98f2dc38 100644 --- a/homeassistant/components/fireservicerota/translations/es-419.json +++ b/homeassistant/components/fireservicerota/translations/es-419.json @@ -1,9 +1,6 @@ { "config": { "step": { - "reauth": { - "description": "Los tokens de autenticaci\u00f3n dejaron de ser v\u00e1lidos, inicie sesi\u00f3n para volver a crearlos." - }, "user": { "data": { "url": "Sitio web" diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index ddd231ce700..ddf96231ae7 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -11,7 +11,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Contrase\u00f1a" }, diff --git a/homeassistant/components/fireservicerota/translations/et.json b/homeassistant/components/fireservicerota/translations/et.json index dedd74e8701..c6400c1adeb 100644 --- a/homeassistant/components/fireservicerota/translations/et.json +++ b/homeassistant/components/fireservicerota/translations/et.json @@ -11,11 +11,11 @@ "invalid_auth": "Vigane autentimine" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Salas\u00f5na" }, - "description": "Tuvastusstring aegus, taasloomiseks logi sisse." + "description": "Autentimise m\u00e4rgised muutusid kehtetuks, logi sisse, et neid uuesti luua." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json index bf663e8ad90..55d130e1d75 100644 --- a/homeassistant/components/fireservicerota/translations/fr.json +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -11,11 +11,10 @@ "invalid_auth": "Authentification non valide" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Mot de passe" - }, - "description": "Les jetons d'authentification ne sont plus valides, connectez-vous pour les recr\u00e9er." + } }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/he.json b/homeassistant/components/fireservicerota/translations/he.json index 61dee20d1ce..79970f0235a 100644 --- a/homeassistant/components/fireservicerota/translations/he.json +++ b/homeassistant/components/fireservicerota/translations/he.json @@ -11,7 +11,7 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } diff --git a/homeassistant/components/fireservicerota/translations/hr.json b/homeassistant/components/fireservicerota/translations/hr.json new file mode 100644 index 00000000000..e1fd87e1f76 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/hr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Lozinka" + }, + "description": "Tokeni za provjeru autenti\u010dnosti postali su neva\u017ee\u0107i, prijavite se da ih ponovno izradite." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json index 3bda2225400..dc4edd2d080 100644 --- a/homeassistant/components/fireservicerota/translations/hu.json +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -11,7 +11,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Jelsz\u00f3" }, diff --git a/homeassistant/components/fireservicerota/translations/id.json b/homeassistant/components/fireservicerota/translations/id.json index 0c4462a1ea7..13430c261ef 100644 --- a/homeassistant/components/fireservicerota/translations/id.json +++ b/homeassistant/components/fireservicerota/translations/id.json @@ -11,7 +11,7 @@ "invalid_auth": "Autentikasi tidak valid" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Kata Sandi" }, diff --git a/homeassistant/components/fireservicerota/translations/it.json b/homeassistant/components/fireservicerota/translations/it.json index 8a437e45900..41bcbb888d2 100644 --- a/homeassistant/components/fireservicerota/translations/it.json +++ b/homeassistant/components/fireservicerota/translations/it.json @@ -11,11 +11,11 @@ "invalid_auth": "Autenticazione non valida" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Password" }, - "description": "I token di autenticazione non sono validi, esegui l'accesso per ricrearli." + "description": "I token di autenticazione non sono pi\u00f9 validi, accedi per ricrearli." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/ja.json b/homeassistant/components/fireservicerota/translations/ja.json index 00358bb1f36..642077d2449 100644 --- a/homeassistant/components/fireservicerota/translations/ja.json +++ b/homeassistant/components/fireservicerota/translations/ja.json @@ -11,12 +11,6 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { - "reauth": { - "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u304c\u7121\u52b9\u306b\u306a\u3063\u305f\u306e\u3067\u3001\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u518d\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/fireservicerota/translations/ka.json b/homeassistant/components/fireservicerota/translations/ka.json index 422f3137d5c..84fed0f1964 100644 --- a/homeassistant/components/fireservicerota/translations/ka.json +++ b/homeassistant/components/fireservicerota/translations/ka.json @@ -11,12 +11,6 @@ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" }, "step": { - "reauth": { - "data": { - "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8" - }, - "description": "\u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e2\u10dd\u10d9\u10d4\u10dc\u10d8 \u10db\u10ea\u10d3\u10d0\u10e0\u10d8\u10d0, \u10e8\u10d4\u10d3\u10d8\u10d7 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d0\u10e8\u10d8 \u10ee\u10d4\u10da\u10d0\u10ee\u10da\u10d0 \u10e8\u10d4\u10e1\u10d0\u10e5\u10db\u10dc\u10d4\u10da\u10d0\u10d3." - }, "user": { "data": { "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8", diff --git a/homeassistant/components/fireservicerota/translations/ko.json b/homeassistant/components/fireservicerota/translations/ko.json index 843371ed035..6326713da73 100644 --- a/homeassistant/components/fireservicerota/translations/ko.json +++ b/homeassistant/components/fireservicerota/translations/ko.json @@ -11,12 +11,6 @@ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { - "reauth": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uc778\uc99d \ud1a0\ud070\uc774 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc0dd\uc131\ud558\ub824\uba74 \ub85c\uadf8\uc778\ud574\uc8fc\uc138\uc694." - }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", diff --git a/homeassistant/components/fireservicerota/translations/lb.json b/homeassistant/components/fireservicerota/translations/lb.json index 9f852c8fdfb..b7473abf339 100644 --- a/homeassistant/components/fireservicerota/translations/lb.json +++ b/homeassistant/components/fireservicerota/translations/lb.json @@ -11,12 +11,6 @@ "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, "step": { - "reauth": { - "data": { - "password": "Passwuert" - }, - "description": "Acc\u00e8s Jetons sin ong\u00eblteg, verbann dech fir se n\u00e9i z'erstellen" - }, "user": { "data": { "password": "Passwuert", diff --git a/homeassistant/components/fireservicerota/translations/nl.json b/homeassistant/components/fireservicerota/translations/nl.json index 62085c9a333..3a7179a7c1f 100644 --- a/homeassistant/components/fireservicerota/translations/nl.json +++ b/homeassistant/components/fireservicerota/translations/nl.json @@ -11,12 +11,6 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { - "reauth": { - "data": { - "password": "Wachtwoord" - }, - "description": "Authenticatietokens zijn ongeldig geworden, log in om ze opnieuw te maken." - }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json index 03ecc365e74..6e11be56f56 100644 --- a/homeassistant/components/fireservicerota/translations/no.json +++ b/homeassistant/components/fireservicerota/translations/no.json @@ -11,11 +11,11 @@ "invalid_auth": "Ugyldig godkjenning" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Passord" }, - "description": "Autentiseringstokener ble ugyldige, logg inn for \u00e5 gjenskape dem." + "description": "Autentiseringstokener ble ugyldige, logg p\u00e5 for \u00e5 gjenskape dem." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/pl.json b/homeassistant/components/fireservicerota/translations/pl.json index 2e5e480fcc1..d4a446aaf09 100644 --- a/homeassistant/components/fireservicerota/translations/pl.json +++ b/homeassistant/components/fireservicerota/translations/pl.json @@ -11,7 +11,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Has\u0142o" }, diff --git a/homeassistant/components/fireservicerota/translations/pt-BR.json b/homeassistant/components/fireservicerota/translations/pt-BR.json index 45b55aa5324..82f4a820ffb 100644 --- a/homeassistant/components/fireservicerota/translations/pt-BR.json +++ b/homeassistant/components/fireservicerota/translations/pt-BR.json @@ -11,7 +11,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Senha" }, diff --git a/homeassistant/components/fireservicerota/translations/pt.json b/homeassistant/components/fireservicerota/translations/pt.json index c78c9a5aba5..7c8a7dfb9d4 100644 --- a/homeassistant/components/fireservicerota/translations/pt.json +++ b/homeassistant/components/fireservicerota/translations/pt.json @@ -11,11 +11,6 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { - "reauth": { - "data": { - "password": "Palavra-passe" - } - }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/fireservicerota/translations/ru.json b/homeassistant/components/fireservicerota/translations/ru.json index 046a65081ec..03be316df52 100644 --- a/homeassistant/components/fireservicerota/translations/ru.json +++ b/homeassistant/components/fireservicerota/translations/ru.json @@ -11,7 +11,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, diff --git a/homeassistant/components/fireservicerota/translations/sk.json b/homeassistant/components/fireservicerota/translations/sk.json index 879d148fd13..3ffaaefa060 100644 --- a/homeassistant/components/fireservicerota/translations/sk.json +++ b/homeassistant/components/fireservicerota/translations/sk.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "create_entry": { @@ -8,6 +9,21 @@ }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Autentifika\u010dn\u00e9 tokeny sa stali neplatn\u00fdmi, prihl\u00e1ste sa a vytvorte ich znova." + }, + "user": { + "data": { + "password": "Heslo", + "url": "Webov\u00e1 lokalita", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/sl.json b/homeassistant/components/fireservicerota/translations/sl.json index e38e7f99169..7aa3a6ccee2 100644 --- a/homeassistant/components/fireservicerota/translations/sl.json +++ b/homeassistant/components/fireservicerota/translations/sl.json @@ -11,12 +11,6 @@ "invalid_auth": "Napaka pri overjanju" }, "step": { - "reauth": { - "data": { - "password": "Geslo" - }, - "description": "Overitveni \u017eetoni niso ve\u010d veljavni, ponovno se prijavite, da jih znova ustvarite." - }, "user": { "data": { "password": "Geslo", diff --git a/homeassistant/components/fireservicerota/translations/sv.json b/homeassistant/components/fireservicerota/translations/sv.json index 79167d65334..23624a5fb81 100644 --- a/homeassistant/components/fireservicerota/translations/sv.json +++ b/homeassistant/components/fireservicerota/translations/sv.json @@ -11,12 +11,6 @@ "invalid_auth": "Ogiltig autentisering" }, "step": { - "reauth": { - "data": { - "password": "L\u00f6senord" - }, - "description": "Autentiseringstokens blev ogiltiga, logga in f\u00f6r att \u00e5terskapa dem." - }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json index 2b9c1b9cb0a..155dcaaf43d 100644 --- a/homeassistant/components/fireservicerota/translations/tr.json +++ b/homeassistant/components/fireservicerota/translations/tr.json @@ -11,12 +11,6 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { - "reauth": { - "data": { - "password": "Parola" - }, - "description": "Kimlik do\u011frulama jetonlar\u0131 ge\u00e7ersiz, yeniden olu\u015fturmak i\u00e7in oturum a\u00e7\u0131n." - }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/fireservicerota/translations/uk.json b/homeassistant/components/fireservicerota/translations/uk.json index 2d3bf8c596e..199120a54ae 100644 --- a/homeassistant/components/fireservicerota/translations/uk.json +++ b/homeassistant/components/fireservicerota/translations/uk.json @@ -11,12 +11,6 @@ "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", diff --git a/homeassistant/components/fireservicerota/translations/zh-Hant.json b/homeassistant/components/fireservicerota/translations/zh-Hant.json index 8e5f4d9f20d..6d1b720296f 100644 --- a/homeassistant/components/fireservicerota/translations/zh-Hant.json +++ b/homeassistant/components/fireservicerota/translations/zh-Hant.json @@ -11,7 +11,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "\u5bc6\u78bc" }, diff --git a/homeassistant/components/firmata/translations/sk.json b/homeassistant/components/firmata/translations/sk.json new file mode 100644 index 00000000000..641bd9c13ee --- /dev/null +++ b/homeassistant/components/firmata/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fitbit/const.py b/homeassistant/components/fitbit/const.py index 1da3058c790..26db1f91659 100644 --- a/homeassistant/components/fitbit/const.py +++ b/homeassistant/components/fitbit/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Final -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -196,18 +196,21 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = ( name="BMI", unit_type="BMI", icon="mdi:human", + state_class=SensorStateClass.MEASUREMENT, ), FitbitSensorEntityDescription( key="body/fat", name="Body Fat", unit_type=PERCENTAGE, icon="mdi:human", + state_class=SensorStateClass.MEASUREMENT, ), FitbitSensorEntityDescription( key="body/weight", name="Weight", unit_type="", icon="mdi:human", + state_class=SensorStateClass.MEASUREMENT, ), FitbitSensorEntityDescription( key="sleep/awakeningsCount", @@ -220,6 +223,7 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = ( name="Sleep Efficiency", unit_type=PERCENTAGE, icon="mdi:sleep", + state_class=SensorStateClass.MEASUREMENT, ), FitbitSensorEntityDescription( key="sleep/minutesAfterWakeup", diff --git a/homeassistant/components/fivem/translations/de.json b/homeassistant/components/fivem/translations/de.json index bb3b3439cbf..bdb2c817e68 100644 --- a/homeassistant/components/fivem/translations/de.json +++ b/homeassistant/components/fivem/translations/de.json @@ -4,8 +4,8 @@ "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Vergewissere dich auch, dass du den neuesten FiveM-Server verwendest.", - "invalid_game_name": "Die API des Spiels, mit dem du dich verbinden willst, ist kein FiveM-Spiel.", + "cannot_connect": "Verbindung fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Vergewissere dich auch, dass du den neuesten FiveM Server verwendest.", + "invalid_game_name": "Die API des Spiels, mit dem du dich verbinden willst, ist kein FiveM Spiel.", "unknown_error": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/fivem/translations/sk.json b/homeassistant/components/fivem/translations/sk.json index 39d2e182c40..f9219d753f0 100644 --- a/homeassistant/components/fivem/translations/sk.json +++ b/homeassistant/components/fivem/translations/sk.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "unknown_error": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov", "port": "Port" } diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 4a6c40fbeae..66de2ddb8a6 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -70,12 +70,14 @@ class Coordinator(DataUpdateCoordinator[State]): log_failures: bool = True, raise_on_auth_failed: bool = False, scheduled: bool = False, + raise_on_entry_error: bool = False, ) -> None: self._refresh_was_scheduled = scheduled await super()._async_refresh( log_failures=log_failures, raise_on_auth_failed=raise_on_auth_failed, scheduled=scheduled, + raise_on_entry_error=raise_on_entry_error, ) async def _async_update_data(self) -> State: diff --git a/homeassistant/components/fjaraskupan/translations/he.json b/homeassistant/components/fjaraskupan/translations/he.json index 380dbc5d7fc..032c9c9fa17 100644 --- a/homeassistant/components/fjaraskupan/translations/he.json +++ b/homeassistant/components/fjaraskupan/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/fjaraskupan/translations/sk.json b/homeassistant/components/fjaraskupan/translations/sk.json new file mode 100644 index 00000000000..99798036ffd --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/manifest.json b/homeassistant/components/flick_electric/manifest.json index 0a79bff792a..849a0270576 100644 --- a/homeassistant/components/flick_electric/manifest.json +++ b/homeassistant/components/flick_electric/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyFlick==0.0.2"], "codeowners": ["@ZephireNZ"], "iot_class": "cloud_polling", + "integration_type": "service", "loggers": ["pyflick"] } diff --git a/homeassistant/components/flick_electric/translations/sk.json b/homeassistant/components/flick_electric/translations/sk.json index 5ada995aa6e..e711bdb9556 100644 --- a/homeassistant/components/flick_electric/translations/sk.json +++ b/homeassistant/components/flick_electric/translations/sk.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "client_id": "ID klienta (volite\u013en\u00e9)", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Prihlasovacie \u00fadaje Flick" + } } } } \ No newline at end of file diff --git a/homeassistant/components/flipr/binary_sensor.py b/homeassistant/components/flipr/binary_sensor.py index 64ccaf7553f..646e260bd60 100644 --- a/homeassistant/components/flipr/binary_sensor.py +++ b/homeassistant/components/flipr/binary_sensor.py @@ -45,7 +45,7 @@ class FliprBinarySensor(FliprEntity, BinarySensorEntity): """Representation of Flipr binary sensors.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on in case of a Problem is detected.""" return ( self.coordinator.data[self.entity_description.key] == "TooLow" diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py index b1e4f31d044..5c8cc6f76fb 100644 --- a/homeassistant/components/flipr/config_flow.py +++ b/homeassistant/components/flipr/config_flow.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult from .const import CONF_FLIPR_ID, DOMAIN @@ -20,12 +21,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _username: str | None = None - _password: str | None = None - _flipr_id: str | None = None - _possible_flipr_ids: list[str] | None = None + _username: str + _password: str + _flipr_id: str = "" + _possible_flipr_ids: list[str] - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the initial step.""" if user_input is None: return self._show_setup_form() @@ -92,7 +95,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return flipr_ids - async def async_step_flipr_id(self, user_input=None): + async def async_step_flipr_id( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the initial step.""" if not user_input: # Creation of a select with the proposal of flipr ids values found by API. diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json index 35fbe8259a2..f88ea64dc3a 100644 --- a/homeassistant/components/flipr/manifest.json +++ b/homeassistant/components/flipr/manifest.json @@ -3,7 +3,7 @@ "name": "Flipr", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flipr", - "requirements": ["flipr-api==1.4.2"], + "requirements": ["flipr-api==1.4.4"], "codeowners": ["@cnico"], "iot_class": "cloud_polling", "loggers": ["flipr_api"] diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index 9cf788d7170..99b44cc95a0 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ELECTRIC_POTENTIAL_MILLIVOLT, TEMP_CELSIUS +from homeassistant.const import ELECTRIC_POTENTIAL_MILLIVOLT, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -48,6 +48,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="battery", + name="Battery Level", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + ), ) @@ -67,6 +74,6 @@ class FliprSensor(FliprEntity, SensorEntity): """Sensor representing FliprSensor data.""" @property - def native_value(self): + def native_value(self) -> str: """State of the sensor.""" return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/flipr/translations/sk.json b/homeassistant/components/flipr/translations/sk.json index 72b0304f1c3..b9832f49cc6 100644 --- a/homeassistant/components/flipr/translations/sk.json +++ b/homeassistant/components/flipr/translations/sk.json @@ -1,13 +1,27 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { + "flipr_id": { + "data": { + "flipr_id": "ID Flipr" + }, + "description": "V zozname vyberte svoje Flipr ID" + }, "user": { "data": { - "email": "Email" - } + "email": "Email", + "password": "Heslo" + }, + "description": "Pripojte sa pomocou svojho \u00fa\u010dtu Flipr.", + "title": "Pripoji\u0165 sa k Flipru" } } } diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index 9cb070a1c62..58e8417de09 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -67,7 +67,6 @@ async def async_setup_entry( class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" - _attr_device_class = SensorDeviceClass.VOLUME _attr_icon = WATER_ICON _attr_native_unit_of_measurement = VOLUME_GALLONS _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING diff --git a/homeassistant/components/flo/translations/sk.json b/homeassistant/components/flo/translations/sk.json index 5ada995aa6e..666f6e28840 100644 --- a/homeassistant/components/flo/translations/sk.json +++ b/homeassistant/components/flo/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flume/translations/bg.json b/homeassistant/components/flume/translations/bg.json index 1ceb53d2be7..56718239f65 100644 --- a/homeassistant/components/flume/translations/bg.json +++ b/homeassistant/components/flume/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/flume/translations/sk.json b/homeassistant/components/flume/translations/sk.json index 71a7aea5018..44f0b9addf1 100644 --- a/homeassistant/components/flume/translations/sk.json +++ b/homeassistant/components/flume/translations/sk.json @@ -1,10 +1,30 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Heslo pre {username} u\u017e nie je platn\u00e9.", + "title": "Znova overte svoj \u00fa\u010det Flume" + }, + "user": { + "data": { + "client_id": "ID klienta", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripoji\u0165 sa k svojmu \u00fa\u010dtu Flume" + } } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/he.json b/homeassistant/components/flux_led/translations/he.json index aa2d7877791..d8290fd672b 100644 --- a/homeassistant/components/flux_led/translations/he.json +++ b/homeassistant/components/flux_led/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/flux_led/translations/sk.json b/homeassistant/components/flux_led/translations/sk.json index bee0999420f..1fe408d6105 100644 --- a/homeassistant/components/flux_led/translations/sk.json +++ b/homeassistant/components/flux_led/translations/sk.json @@ -1,7 +1,32 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Zvolen\u00fd re\u017eim jasu." + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index 600ed363a8b..e74585da35b 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Forecast.Solar integration.""" from __future__ import annotations +import re from typing import Any import voluptuous as vol @@ -9,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from .const import ( CONF_AZIMUTH, @@ -20,6 +21,8 @@ from .const import ( DOMAIN, ) +RE_API_KEY = re.compile(r"^[a-zA-Z0-9]{16}$") + class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Forecast.Solar.""" @@ -88,8 +91,16 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" + errors = {} if user_input is not None: - return self.async_create_entry(title="", data=user_input) + if (api_key := user_input.get(CONF_API_KEY)) and RE_API_KEY.match( + api_key + ) is None: + errors[CONF_API_KEY] = "invalid_api_key" + else: + return self.async_create_entry( + title="", data=user_input | {CONF_API_KEY: api_key or None} + ) return self.async_show_form( step_id="init", @@ -129,4 +140,5 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): ): vol.Coerce(int), } ), + errors=errors, ) diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index b10e927eb8b..a7bc0190f5f 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -15,9 +15,12 @@ } }, "options": { + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, "step": { "init": { - "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear.", + "description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear.", "data": { "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", diff --git a/homeassistant/components/forecast_solar/translations/bg.json b/homeassistant/components/forecast_solar/translations/bg.json index 289146783a4..210c38c6225 100644 --- a/homeassistant/components/forecast_solar/translations/bg.json +++ b/homeassistant/components/forecast_solar/translations/bg.json @@ -13,6 +13,9 @@ } }, "options": { + "error": { + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/ca.json b/homeassistant/components/forecast_solar/translations/ca.json index 141ad11c165..65d1c3100a7 100644 --- a/homeassistant/components/forecast_solar/translations/ca.json +++ b/homeassistant/components/forecast_solar/translations/ca.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Clau API inv\u00e0lida" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Pot\u00e8ncia de l'inversor (Watts)", "modules power": "Pot\u00e8ncia m\u00e0xima total dels panells solars" }, - "description": "Aquests valors permeten ajustar els resultats de Solar.Forecast. Consulta la documentaci\u00f3 si tens dubtes sobre algun camp." + "description": "Aquests valors permeten ajustar els resultats de Forecast.Solar. Consulta la documentaci\u00f3 si tens dubtes sobre algun camp." } } } diff --git a/homeassistant/components/forecast_solar/translations/cs.json b/homeassistant/components/forecast_solar/translations/cs.json index d1c9b95470f..0fe5725fd01 100644 --- a/homeassistant/components/forecast_solar/translations/cs.json +++ b/homeassistant/components/forecast_solar/translations/cs.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/de.json b/homeassistant/components/forecast_solar/translations/de.json index 06e51e04659..4466dd15fcc 100644 --- a/homeassistant/components/forecast_solar/translations/de.json +++ b/homeassistant/components/forecast_solar/translations/de.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Wechselrichtergr\u00f6\u00dfe (Watt)", "modules power": "Gesamt-Watt-Spitzenleistung deiner Solarmodule" }, - "description": "Mit diesen Werten kann das Solar.Forecast-Ergebnis angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach." + "description": "Mit diesen Werten kann das Ergebnis von Forecast.Solar angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach." } } } diff --git a/homeassistant/components/forecast_solar/translations/el.json b/homeassistant/components/forecast_solar/translations/el.json index e2e75c05f65..31e2708207f 100644 --- a/homeassistant/components/forecast_solar/translations/el.json +++ b/homeassistant/components/forecast_solar/translations/el.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json index 2aa5a37cd1c..35542dcdf75 100644 --- a/homeassistant/components/forecast_solar/translations/en.json +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Invalid API key" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Inverter size (Watt)", "modules power": "Total Watt peak power of your solar modules" }, - "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear." + "description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear." } } } diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 08e67ec95d1..227eeab1351 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Clave API no v\u00e1lida" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Tama\u00f1o del inversor (vatios)", "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares" }, - "description": "Estos valores permiten modificar el resultado de Solar.Forecast. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." + "description": "Estos valores permiten modificar el resultado de Forecast.Solar. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } } diff --git a/homeassistant/components/forecast_solar/translations/et.json b/homeassistant/components/forecast_solar/translations/et.json index d2b72b30708..2c611bafeba 100644 --- a/homeassistant/components/forecast_solar/translations/et.json +++ b/homeassistant/components/forecast_solar/translations/et.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Kehtetu API v\u00f5ti" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Inverteri v\u00f5imsus (vatti)", "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides" }, - "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui asi on ebaselge." + "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui see v\u00e4li on ebaselge." } } } diff --git a/homeassistant/components/forecast_solar/translations/fr.json b/homeassistant/components/forecast_solar/translations/fr.json index 78b4d3f8f88..d2591b6915b 100644 --- a/homeassistant/components/forecast_solar/translations/fr.json +++ b/homeassistant/components/forecast_solar/translations/fr.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Cl\u00e9 d'API non valide" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 3aac09afe1b..7ff078d1301 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -25,7 +25,7 @@ "inverter_size": "Inverter m\u00e9rete (Watt)", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" }, - "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 kit\u00f6lt\u00e9se nem egy\u00e9rtelm\u0171." + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Forecast.Solar eredm\u00e9ny\u00e9nek finomhangol\u00e1s\u00e1t. Ha egy mez\u0151 nem egy\u00e9rtelm\u0171, k\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t." } } } diff --git a/homeassistant/components/forecast_solar/translations/id.json b/homeassistant/components/forecast_solar/translations/id.json index 5bd1236d6a6..66b993e3ae6 100644 --- a/homeassistant/components/forecast_solar/translations/id.json +++ b/homeassistant/components/forecast_solar/translations/id.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Kunci API tidak valid" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Ukuran inverter (Watt)", "modules power": "Total daya puncak modul surya Anda dalam Watt" }, - "description": "Nilai-nilai ini memungkinkan penyesuaian hasil Solar.Forecast. Rujuk ke dokumentasi jika bidang isian tidak jelas." + "description": "Nilai-nilai ini memungkinkan penyesuaian hasil Forecast.Solar. Rujuk ke dokumentasi jika bidang isian tidak jelas." } } } diff --git a/homeassistant/components/forecast_solar/translations/it.json b/homeassistant/components/forecast_solar/translations/it.json index 598c67695cc..8d6d266fa95 100644 --- a/homeassistant/components/forecast_solar/translations/it.json +++ b/homeassistant/components/forecast_solar/translations/it.json @@ -25,7 +25,7 @@ "inverter_size": "Dimensioni inverter (Watt)", "modules power": "Potenza di picco totale in Watt dei tuoi moduli solari" }, - "description": "Questi valori consentono di modificare il risultato di Solar.Forecast. Fai riferimento alla documentazione se un campo non \u00e8 chiaro." + "description": "Questi valori consentono di modificare il risultato di Forecast.Solar. Se un campo non \u00e8 chiaro, consultare la documentazione." } } } diff --git a/homeassistant/components/forecast_solar/translations/nl.json b/homeassistant/components/forecast_solar/translations/nl.json index dbc966e59fc..3e92695648e 100644 --- a/homeassistant/components/forecast_solar/translations/nl.json +++ b/homeassistant/components/forecast_solar/translations/nl.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Ongeldige API sleutel" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/no.json b/homeassistant/components/forecast_solar/translations/no.json index a9acbb86f00..da9368c3646 100644 --- a/homeassistant/components/forecast_solar/translations/no.json +++ b/homeassistant/components/forecast_solar/translations/no.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Inverterst\u00f8rrelse (Watt)", "modules power": "Total Watt-toppeffekt i solcellemodulene dine" }, - "description": "Disse verdiene tillater justering av Solar.Forecast -resultatet. Se dokumentasjonen hvis et felt er uklart." + "description": "Disse verdiene gj\u00f8r det mulig \u00e5 justere Forecast.Solar-resultatet. Vennligst se dokumentasjonen hvis et felt er uklart." } } } diff --git a/homeassistant/components/forecast_solar/translations/pl.json b/homeassistant/components/forecast_solar/translations/pl.json index ad01ce4bb54..c4be17eed34 100644 --- a/homeassistant/components/forecast_solar/translations/pl.json +++ b/homeassistant/components/forecast_solar/translations/pl.json @@ -25,7 +25,7 @@ "inverter_size": "Rozmiar falownika (Wat)", "modules power": "Ca\u0142kowita moc szczytowa modu\u0142\u00f3w fotowoltaicznych w watach" }, - "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Solar.Forecast. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." + "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Forecast.Solar. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." } } } diff --git a/homeassistant/components/forecast_solar/translations/pt-BR.json b/homeassistant/components/forecast_solar/translations/pt-BR.json index 6761e17e8bd..2fe7ed3e900 100644 --- a/homeassistant/components/forecast_solar/translations/pt-BR.json +++ b/homeassistant/components/forecast_solar/translations/pt-BR.json @@ -10,11 +10,14 @@ "modules power": "Pot\u00eancia de pico total em Watt de seus m\u00f3dulos solares", "name": "Nome" }, - "description": "Preencha os dados de seus pain\u00e9is solares. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." + "description": "Esses valores permitem ajustar o resultado do Forecast.Solar. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." } } }, "options": { + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "Pot\u00eancia do inversor (Watt)", "modules power": "Pot\u00eancia de pico total em Watt de seus m\u00f3dulos solares" }, - "description": "Preencha os dados de seus pain\u00e9is solares. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." + "description": "Esses valores permitem ajustar o resultado do Forecast.Solar. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." } } } diff --git a/homeassistant/components/forecast_solar/translations/ru.json b/homeassistant/components/forecast_solar/translations/ru.json index f7d4d502691..88c6407d324 100644 --- a/homeassistant/components/forecast_solar/translations/ru.json +++ b/homeassistant/components/forecast_solar/translations/ru.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/sk.json b/homeassistant/components/forecast_solar/translations/sk.json index 939157fb837..ff539ab78db 100644 --- a/homeassistant/components/forecast_solar/translations/sk.json +++ b/homeassistant/components/forecast_solar/translations/sk.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/zh-Hans.json b/homeassistant/components/forecast_solar/translations/zh-Hans.json index 8a667cf9260..7d2e7846f2f 100644 --- a/homeassistant/components/forecast_solar/translations/zh-Hans.json +++ b/homeassistant/components/forecast_solar/translations/zh-Hans.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "\u65e0\u6548\u7684API\u5bc6\u94a5" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/zh-Hant.json b/homeassistant/components/forecast_solar/translations/zh-Hant.json index 3870ca29846..1d044729708 100644 --- a/homeassistant/components/forecast_solar/translations/zh-Hant.json +++ b/homeassistant/components/forecast_solar/translations/zh-Hant.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "\u8b8a\u6d41\u5668\u5c3a\u5bf8\uff08Watt\uff09", "modules power": "\u7e3d\u5cf0\u503c\u529f\u7387" }, - "description": "\u6b64\u4e9b\u6578\u503c\u5141\u8a31\u5fae\u8abf Solar.Forecast \u7d50\u679c\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\u3001\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002" + "description": "\u6b64\u4e9b\u6578\u503c\u5141\u8a31\u5fae\u8abf Forecast.Solar \u7d50\u679c\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\u3001\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002" } } } diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 9da1c1a1168..c60a90176a9 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEnqueue, MediaPlayerEntity, + MediaPlayerEntityFeature, MediaPlayerState, MediaType, async_process_play_media_url, @@ -236,7 +237,7 @@ class ForkedDaapdZone(MediaPlayerEntity): await self._api.set_volume(volume=volume * 100, output_id=self._output_id) @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" return SUPPORTED_FEATURES_ZONE @@ -558,7 +559,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): return self._player["shuffle"] @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" return SUPPORTED_FEATURES @@ -925,7 +926,8 @@ class ForkedDaapdUpdater: else: _LOGGER.error("Invalid websocket port") - def _disconnected_callback(self): + async def _disconnected_callback(self): + """Send update signals when the websocket gets disconnected.""" async_dispatcher_send( self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), False ) diff --git a/homeassistant/components/forked_daapd/translations/sk.json b/homeassistant/components/forked_daapd/translations/sk.json new file mode 100644 index 00000000000..950f5c33621 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "unknown_error": "Neo\u010dak\u00e1van\u00e1 chyba", + "wrong_host_or_port": "Ned\u00e1 sa pripoji\u0165. Skontrolujte hostite\u013ea a port.", + "wrong_password": "Nespr\u00e1vne heslo." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo API (ak heslo nem\u00e1te, nechajte pr\u00e1zdne)", + "port": "API port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/bg.json b/homeassistant/components/foscam/translations/bg.json index 8e0b1bac052..8318b416845 100644 --- a/homeassistant/components/foscam/translations/bg.json +++ b/homeassistant/components/foscam/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { @@ -7,6 +10,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "rtsp_port": "RTSP \u043f\u043e\u0440\u0442", + "stream": "\u041f\u043e\u0442\u043e\u043a", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/foscam/translations/sk.json b/homeassistant/components/foscam/translations/sk.json index 8bbcb516b56..701645cc48c 100644 --- a/homeassistant/components/foscam/translations/sk.json +++ b/homeassistant/components/foscam/translations/sk.json @@ -4,12 +4,18 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_response": "Neplatn\u00e1 odpove\u010f zo zariadenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { + "host": "Hostite\u013e", + "password": "Heslo", "port": "Port", + "rtsp_port": "RTSP port", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 0b48d08170a..7232f16696e 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -4,8 +4,7 @@ from __future__ import annotations from datetime import datetime from typing import Any -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/freebox/translations/bg.json b/homeassistant/components/freebox/translations/bg.json index 9a63019cd8a..dfb1e6f4932 100644 --- a/homeassistant/components/freebox/translations/bg.json +++ b/homeassistant/components/freebox/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/freebox/translations/sk.json b/homeassistant/components/freebox/translations/sk.json index 892b8b2cd91..41da4a04be1 100644 --- a/homeassistant/components/freebox/translations/sk.json +++ b/homeassistant/components/freebox/translations/sk.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "register_failed": "Registr\u00e1cia zlyhala, sk\u00faste to znova", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/freedompro/translations/sk.json b/homeassistant/components/freedompro/translations/sk.json index ff853127803..6a9b7374d51 100644 --- a/homeassistant/components/freedompro/translations/sk.json +++ b/homeassistant/components/freedompro/translations/sk.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "user": { "data": { "api_key": "API k\u013e\u00fa\u010d" - } + }, + "description": "Zadajte k\u013e\u00fa\u010d API z\u00edskan\u00fd z https://home.freedompro.eu" } } } diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 6364ada9fb2..5af4f0c2239 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -663,6 +663,14 @@ class AvmWrapper(FritzBoxTools): partial(self.get_wan_link_properties) ) + async def async_ipv6_active(self) -> bool: + """Check ip an ipv6 is active on the WAn interface.""" + + def wrap_external_ipv6() -> str: + return str(self.fritz_status.external_ipv6) + + return bool(await self.hass.async_add_executor_job(wrap_external_ipv6)) + async def async_get_connection_info(self) -> ConnectionInfo: """Return ConnectionInfo data.""" @@ -671,6 +679,7 @@ class AvmWrapper(FritzBoxTools): connection=link_properties.get("NewWANAccessType", "").lower(), mesh_role=self.mesh_role, wan_enabled=self.device_is_router, + ipv6_active=await self.async_ipv6_active(), ) _LOGGER.debug( "ConnectionInfo for FritzBox %s: %s", @@ -1011,3 +1020,4 @@ class ConnectionInfo: connection: str mesh_role: MeshRoles wan_enabled: bool + ipv6_active: bool diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 998eab2ede7..212710a638c 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -4,8 +4,7 @@ from __future__ import annotations import datetime import logging -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 0f3d8cb1ae0..821a0000a5e 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -66,6 +66,11 @@ def _retrieve_external_ip_state(status: FritzStatus, last_value: str) -> str: return status.external_ip # type: ignore[no-any-return] +def _retrieve_external_ipv6_state(status: FritzStatus, last_value: str) -> str: + """Return external ipv6 from device.""" + return str(status.external_ipv6) + + def _retrieve_kb_s_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload transmission rate.""" return round(status.transmission_rate[0] / 1000, 1) # type: ignore[no-any-return] @@ -155,6 +160,13 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( icon="mdi:earth", value_fn=_retrieve_external_ip_state, ), + FritzSensorEntityDescription( + key="external_ipv6", + name="External IPv6", + icon="mdi:earth", + value_fn=_retrieve_external_ipv6_state, + is_suitable=lambda info: info.ipv6_active, + ), FritzSensorEntityDescription( key="device_uptime", name="Device Uptime", diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index 7341a275d46..5b2be26d0d4 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index c4ca8595f52..7c7e0e13380 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -9,7 +9,8 @@ "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", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "upnp_not_configured": "Na za\u0159\u00edzen\u00ed chyb\u00ed nastaven\u00ed UPnP." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 16d2be68adb..fc5e522e18c 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "ignore_ip6_link_local": "IPv6 link local address wird nicht unterst\u00fctzt.", + "ignore_ip6_link_local": "IPv6 Link-Local-Adresse wird nicht unterst\u00fctzt.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index b1fa200731e..bb18e38d1eb 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -20,7 +20,7 @@ "password": "Password", "username": "Nome utente" }, - "description": "FRITZ! Box rilevato: {name} \n\n Configura gli strumenti del FRITZ! Box per controllare il tuo {name}", + "description": "FRITZ! Box rilevato: {name} \n\nConfigura gli strumenti del FRITZ! Box per controllare il tuo {name}", "title": "Configura gli strumenti del FRITZ!Box" }, "reauth_confirm": { diff --git a/homeassistant/components/fritz/translations/sk.json b/homeassistant/components/fritz/translations/sk.json index 17cb7bfc78b..2dae4dc6f85 100644 --- a/homeassistant/components/fritz/translations/sk.json +++ b/homeassistant/components/fritz/translations/sk.json @@ -1,17 +1,52 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "ignore_ip6_link_local": "Lok\u00e1lna adresa odkazu IPv6 nie je podporovan\u00e1.", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "upnp_not_configured": "Ch\u00fdbaj\u00face nastavenia UPnP v zariaden\u00ed." }, + "flow_title": "{name}", "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Objavil FRITZ!Box: {name} \n\n Nastavte n\u00e1stroje FRITZ!Box na ovl\u00e1danie v\u00e1\u0161ho {name}", + "title": "Nastavte n\u00e1stroje FRITZ!Box" + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Aktualizujte poverenia FRITZ!Box Tools pre: {host}. \n\n FRITZ!Box Tools sa nedok\u00e1\u017ee prihl\u00e1si\u0165 do v\u00e1\u0161ho FRITZ!Box.", + "title": "Aktualiz\u00e1cia FRITZ!Box Tools \u2013 poverenia" + }, "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Nastavte n\u00e1stroje FRITZ!Box" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "old_discovery": "Povoli\u0165 star\u00fa met\u00f3du zis\u0165ovania" } } } diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 4bb2eee45b1..40d170db3b3 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,7 +1,10 @@ """Support for AVM FRITZ!SmartHome devices.""" from __future__ import annotations +from abc import ABC, abstractmethod + from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -93,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator]): +class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator], ABC): """Basis FritzBox entity.""" def __init__( @@ -108,30 +111,39 @@ class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator]): self.ain = ain if entity_description is not None: self.entity_description = entity_description - self._attr_name = f"{self.device.name} {entity_description.name}" + self._attr_name = f"{self.data.name} {entity_description.name}" self._attr_unique_id = f"{ain}_{entity_description.key}" else: - self._attr_name = self.device.name + self._attr_name = self.data.name self._attr_unique_id = ain + @property + @abstractmethod + def data(self) -> FritzhomeEntityBase: + """Return data object from coordinator.""" + + +class FritzBoxDeviceEntity(FritzBoxEntity): + """Reflects FritzhomeDevice and uses its attributes to construct FritzBoxDeviceEntity.""" + @property def available(self) -> bool: """Return if entity is available.""" - return super().available and self.device.present + return super().available and self.data.present @property - def device(self) -> FritzhomeDevice: - """Return device object from coordinator.""" - return self.coordinator.data[self.ain] + def data(self) -> FritzhomeDevice: + """Return device data object from coordinator.""" + return self.coordinator.data.devices[self.ain] @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( - name=self.device.name, + name=self.data.name, identifiers={(DOMAIN, self.ain)}, - manufacturer=self.device.manufacturer, - model=self.device.productname, - sw_version=self.device.fw_version, + manufacturer=self.data.manufacturer, + model=self.data.productname, + sw_version=self.data.fw_version, configuration_url=self.coordinator.configuration_url, ) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index b80d853e562..62f0786af53 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FritzBoxEntity +from . import FritzBoxDeviceEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzEntityDescriptionMixinBase @@ -68,19 +68,21 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome binary sensor from ConfigEntry.""" - coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + coordinator: FritzboxDataUpdateCoordinator = hass.data[FRITZBOX_DOMAIN][ + entry.entry_id + ][CONF_COORDINATOR] async_add_entities( [ FritzboxBinarySensor(coordinator, ain, description) - for ain, device in coordinator.data.items() + for ain, device in coordinator.data.devices.items() for description in BINARY_SENSOR_TYPES if description.suitable(device) ] ) -class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): +class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity): """Representation of a binary FRITZ!SmartHome device.""" entity_description: FritzBinarySensorEntityDescription @@ -93,10 +95,10 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): ) -> None: """Initialize the FritzBox entity.""" super().__init__(coordinator, ain, entity_description) - self._attr_name = f"{self.device.name} {entity_description.name}" + self._attr_name = f"{self.data.name} {entity_description.name}" self._attr_unique_id = f"{ain}_{entity_description.key}" @property def is_on(self) -> bool | None: """Return true if sensor is on.""" - return self.entity_description.is_on(self.device) + return self.entity_description.is_on(self.data) diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py new file mode 100644 index 00000000000..00e8ad5e4d6 --- /dev/null +++ b/homeassistant/components/fritzbox/button.py @@ -0,0 +1,56 @@ +"""Support for AVM FRITZ!SmartHome templates.""" +from pyfritzhome.devicetypes import FritzhomeTemplate + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FritzboxDataUpdateCoordinator, FritzBoxEntity +from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the FRITZ!SmartHome template from ConfigEntry.""" + coordinator: FritzboxDataUpdateCoordinator = hass.data[FRITZBOX_DOMAIN][ + entry.entry_id + ][CONF_COORDINATOR] + + async_add_entities( + [ + FritzBoxTemplate(coordinator, ain) + for ain in coordinator.data.templates.keys() + ] + ) + + +class FritzBoxTemplate(FritzBoxEntity, ButtonEntity): + """Interface between FritzhomeTemplate and hass.""" + + @property + def data(self) -> FritzhomeTemplate: + """Return the template data entity.""" + return self.coordinator.data.templates[self.ain] + + @property + def device_info(self) -> DeviceInfo: + """Return device specific attributes.""" + return DeviceInfo( + name=self.data.name, + identifiers={(FRITZBOX_DOMAIN, self.ain)}, + configuration_url=self.coordinator.configuration_url, + manufacturer="AVM", + model="SmartHome Template", + ) + + async def async_press(self) -> None: + """Apply template and refresh.""" + await self.hass.async_add_executor_job(self.apply_template) + await self.coordinator.async_refresh() + + def apply_template(self) -> None: + """Use Fritzhome to apply the template via ain.""" + self.coordinator.fritz.apply_template(self.ain) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 806f8b2303e..b1898c41cc7 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -21,7 +21,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FritzBoxEntity +from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity from .const import ( ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, @@ -50,18 +50,20 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome thermostat from ConfigEntry.""" - coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + coordinator: FritzboxDataUpdateCoordinator = hass.data[FRITZBOX_DOMAIN][ + entry.entry_id + ][CONF_COORDINATOR] async_add_entities( [ FritzboxThermostat(coordinator, ain) - for ain, device in coordinator.data.items() + for ain, device in coordinator.data.devices.items() if device.has_thermostat ] ) -class FritzboxThermostat(FritzBoxEntity, ClimateEntity): +class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): """The thermostat class for FRITZ!SmartHome thermostats.""" _attr_precision = PRECISION_HALVES @@ -73,18 +75,18 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): @property def current_temperature(self) -> float: """Return the current temperature.""" - if self.device.has_temperature_sensor and self.device.temperature is not None: - return self.device.temperature # type: ignore [no-any-return] - return self.device.actual_temperature # type: ignore [no-any-return] + if self.data.has_temperature_sensor and self.data.temperature is not None: + return self.data.temperature # type: ignore [no-any-return] + return self.data.actual_temperature # type: ignore [no-any-return] @property def target_temperature(self) -> float: """Return the temperature we try to reach.""" - if self.device.target_temperature == ON_API_TEMPERATURE: + if self.data.target_temperature == ON_API_TEMPERATURE: return ON_REPORT_SET_TEMPERATURE - if self.device.target_temperature == OFF_API_TEMPERATURE: + if self.data.target_temperature == OFF_API_TEMPERATURE: return OFF_REPORT_SET_TEMPERATURE - return self.device.target_temperature # type: ignore [no-any-return] + return self.data.target_temperature # type: ignore [no-any-return] async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -94,14 +96,14 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): elif kwargs.get(ATTR_TEMPERATURE) is not None: temperature = kwargs[ATTR_TEMPERATURE] await self.hass.async_add_executor_job( - self.device.set_target_temperature, temperature + self.data.set_target_temperature, temperature ) await self.coordinator.async_refresh() @property def hvac_mode(self) -> str: """Return the current operation mode.""" - if self.device.target_temperature in ( + if self.data.target_temperature in ( OFF_REPORT_SET_TEMPERATURE, OFF_API_TEMPERATURE, ): @@ -119,16 +121,14 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): if hvac_mode == HVACMode.OFF: await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) else: - await self.async_set_temperature( - temperature=self.device.comfort_temperature - ) + await self.async_set_temperature(temperature=self.data.comfort_temperature) @property def preset_mode(self) -> str | None: """Return current preset mode.""" - if self.device.target_temperature == self.device.comfort_temperature: + if self.data.target_temperature == self.data.comfort_temperature: return PRESET_COMFORT - if self.device.target_temperature == self.device.eco_temperature: + if self.data.target_temperature == self.data.eco_temperature: return PRESET_ECO return None @@ -140,11 +140,9 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" if preset_mode == PRESET_COMFORT: - await self.async_set_temperature( - temperature=self.device.comfort_temperature - ) + await self.async_set_temperature(temperature=self.data.comfort_temperature) elif preset_mode == PRESET_ECO: - await self.async_set_temperature(temperature=self.device.eco_temperature) + await self.async_set_temperature(temperature=self.data.eco_temperature) @property def min_temp(self) -> int: @@ -160,17 +158,17 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity): def extra_state_attributes(self) -> ClimateExtraAttributes: """Return the device specific state attributes.""" attrs: ClimateExtraAttributes = { - ATTR_STATE_BATTERY_LOW: self.device.battery_low, + ATTR_STATE_BATTERY_LOW: self.data.battery_low, } # the following attributes are available since fritzos 7 - if self.device.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = self.device.battery_level - if self.device.holiday_active is not None: - attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active - if self.device.summer_active is not None: - attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active - if self.device.window_open is not None: - attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open + if self.data.battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self.data.battery_level + if self.data.holiday_active is not None: + attrs[ATTR_STATE_HOLIDAY_MODE] = self.data.holiday_active + if self.data.summer_active is not None: + attrs[ATTR_STATE_SUMMER_MODE] = self.data.summer_active + if self.data.window_open is not None: + attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open return attrs diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 4831b0f6ab2..791da4540a4 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -26,6 +26,7 @@ LOGGER: Final[logging.Logger] = logging.getLogger(__package__) PLATFORMS: Final[list[Platform]] = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.LIGHT, diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 47d2cdca005..c16751c8c9f 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -1,9 +1,11 @@ """Data update coordinator for AVM FRITZ!SmartHome devices.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +from pyfritzhome.devicetypes import FritzhomeTemplate import requests from homeassistant.config_entries import ConfigEntry @@ -14,7 +16,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_CONNECTIONS, DOMAIN, LOGGER -class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): +@dataclass +class FritzboxCoordinatorData: + """Data Type of FritzboxDataUpdateCoordinator's data.""" + + devices: dict[str, FritzhomeDevice] + templates: dict[str, FritzhomeTemplate] + + +class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]): """Fritzbox Smarthome device data update coordinator.""" configuration_url: str @@ -31,10 +41,11 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=30), ) - def _update_fritz_devices(self) -> dict[str, FritzhomeDevice]: + def _update_fritz_devices(self) -> FritzboxCoordinatorData: """Update all fritzbox device data.""" try: self.fritz.update_devices() + self.fritz.update_templates() except requests.exceptions.ConnectionError as ex: raise UpdateFailed from ex except requests.exceptions.HTTPError: @@ -44,9 +55,10 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): except LoginError as ex: raise ConfigEntryAuthFailed from ex self.fritz.update_devices() + self.fritz.update_templates() devices = self.fritz.get_devices() - data = {} + device_data = {} for device in devices: # assume device as unavailable, see #55799 if ( @@ -61,9 +73,15 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): LOGGER.debug("Assume device %s as unavailable", device.name) device.present = False - data[device.ain] = device - return data + device_data[device.ain] = device - async def _async_update_data(self) -> dict[str, FritzhomeDevice]: + templates = self.fritz.get_templates() + template_data = {} + for template in templates: + template_data[template.ain] = template + + return FritzboxCoordinatorData(devices=device_data, templates=template_data) + + async def _async_update_data(self) -> FritzboxCoordinatorData: """Fetch all device data.""" return await self.hass.async_add_executor_job(self._update_fritz_devices) diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py index d8fc8d4f3c3..df3b1562f9b 100644 --- a/homeassistant/components/fritzbox/cover.py +++ b/homeassistant/components/fritzbox/cover.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FritzBoxEntity +from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN @@ -21,16 +21,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome cover from ConfigEntry.""" - coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + coordinator: FritzboxDataUpdateCoordinator = hass.data[FRITZBOX_DOMAIN][ + entry.entry_id + ][CONF_COORDINATOR] async_add_entities( FritzboxCover(coordinator, ain) - for ain, device in coordinator.data.items() + for ain, device in coordinator.data.devices.items() if device.has_blind ) -class FritzboxCover(FritzBoxEntity, CoverEntity): +class FritzboxCover(FritzBoxDeviceEntity, CoverEntity): """The cover class for FRITZ!SmartHome covers.""" _attr_device_class = CoverDeviceClass.BLIND @@ -45,34 +47,34 @@ class FritzboxCover(FritzBoxEntity, CoverEntity): def current_cover_position(self) -> int | None: """Return the current position.""" position = None - if self.device.levelpercentage is not None: - position = 100 - self.device.levelpercentage + if self.data.levelpercentage is not None: + position = 100 - self.data.levelpercentage return position @property def is_closed(self) -> bool | None: """Return if the cover is closed.""" - if self.device.levelpercentage is None: + if self.data.levelpercentage is None: return None - return self.device.levelpercentage == 100 # type: ignore [no-any-return] + return self.data.levelpercentage == 100 # type: ignore [no-any-return] async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self.hass.async_add_executor_job(self.device.set_blind_open) + await self.hass.async_add_executor_job(self.data.set_blind_open) await self.coordinator.async_refresh() async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self.hass.async_add_executor_job(self.device.set_blind_close) + await self.hass.async_add_executor_job(self.data.set_blind_close) await self.coordinator.async_refresh() async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" await self.hass.async_add_executor_job( - self.device.set_level_percentage, 100 - kwargs[ATTR_POSITION] + self.data.set_level_percentage, 100 - kwargs[ATTR_POSITION] ) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - await self.hass.async_add_executor_job(self.device.set_blind_stop) + await self.hass.async_add_executor_job(self.data.set_blind_stop) await self.coordinator.async_refresh() diff --git a/homeassistant/components/fritzbox/diagnostics.py b/homeassistant/components/fritzbox/diagnostics.py index 581fe4c6f01..403082c4f90 100644 --- a/homeassistant/components/fritzbox/diagnostics.py +++ b/homeassistant/components/fritzbox/diagnostics.py @@ -23,11 +23,13 @@ async def async_get_config_entry_diagnostics( "entry": async_redact_data(entry.as_dict(), TO_REDACT), "data": {}, } - if not isinstance(coordinator.data, dict): - return diag_data + entities: dict[str, dict] = { + **coordinator.data.devices, + **coordinator.data.templates, + } diag_data["data"] = { - ain: {k: v for k, v in vars(dev).items() if not k.startswith("_")} - for ain, dev in coordinator.data.items() + ain: {k: v for k, v in vars(entity).items() if not k.startswith("_")} + for ain, entity in entities.items() } return diag_data diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 0f7037843d8..03eb9eb9d78 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -1,7 +1,7 @@ """Support for AVM FRITZ!SmartHome lightbulbs.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from requests.exceptions import HTTPError @@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FritzBoxEntity +from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity from .const import ( COLOR_MODE, COLOR_TEMP_MODE, @@ -24,7 +24,6 @@ from .const import ( DOMAIN as FRITZBOX_DOMAIN, LOGGER, ) -from .coordinator import FritzboxDataUpdateCoordinator SUPPORTED_COLOR_MODES = {ColorMode.COLOR_TEMP, ColorMode.HS} @@ -34,9 +33,11 @@ async def async_setup_entry( ) -> None: """Set up the FRITZ!SmartHome light from ConfigEntry.""" entities: list[FritzboxLight] = [] - coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + coordinator: FritzboxDataUpdateCoordinator = hass.data[FRITZBOX_DOMAIN][ + entry.entry_id + ][CONF_COORDINATOR] - for ain, device in coordinator.data.items(): + for ain, device in coordinator.data.devices.items(): if not device.has_lightbulb: continue @@ -58,7 +59,7 @@ async def async_setup_entry( async_add_entities(entities) -class FritzboxLight(FritzBoxEntity, LightEntity): +class FritzboxLight(FritzBoxDeviceEntity, LightEntity): """The light class for FRITZ!SmartHome lightbulbs.""" def __init__( @@ -76,7 +77,7 @@ class FritzboxLight(FritzBoxEntity, LightEntity): # Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each. # Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup - self._supported_hs = {} + self._supported_hs: dict[int, list[int]] = {} for values in supported_colors.values(): hue = int(values[0][0]) self._supported_hs[hue] = [ @@ -88,36 +89,36 @@ class FritzboxLight(FritzBoxEntity, LightEntity): @property def is_on(self) -> bool: """If the light is currently on or off.""" - return self.device.state # type: ignore [no-any-return] + return self.data.state # type: ignore [no-any-return] @property def brightness(self) -> int: """Return the current Brightness.""" - return self.device.level # type: ignore [no-any-return] + return self.data.level # type: ignore [no-any-return] @property def hs_color(self) -> tuple[float, float] | None: """Return the hs color value.""" - if self.device.color_mode != COLOR_MODE: + if self.data.color_mode != COLOR_MODE: return None - hue = self.device.hue - saturation = self.device.saturation + hue = self.data.hue + saturation = self.data.saturation return (hue, float(saturation) * 100.0 / 255.0) @property def color_temp_kelvin(self) -> int | None: """Return the CT color value.""" - if self.device.color_mode != COLOR_TEMP_MODE: + if self.data.color_mode != COLOR_TEMP_MODE: return None - return self.device.color_temp # type: ignore [no-any-return] + return self.data.color_temp # type: ignore [no-any-return] @property def color_mode(self) -> ColorMode: """Return the color mode of the light.""" - if self.device.color_mode == COLOR_MODE: + if self.data.color_mode == COLOR_MODE: return ColorMode.HS return ColorMode.COLOR_TEMP @@ -130,16 +131,18 @@ class FritzboxLight(FritzBoxEntity, LightEntity): """Turn the light on.""" if kwargs.get(ATTR_BRIGHTNESS) is not None: level = kwargs[ATTR_BRIGHTNESS] - await self.hass.async_add_executor_job(self.device.set_level, level) + await self.hass.async_add_executor_job(self.data.set_level, level) if kwargs.get(ATTR_HS_COLOR) is not None: # Try setunmappedcolor first. This allows free color selection, # but we don't know if its supported by all devices. try: # HA gives 0..360 for hue, fritz light only supports 0..359 unmapped_hue = int(kwargs[ATTR_HS_COLOR][0] % 360) - unmapped_saturation = round(kwargs[ATTR_HS_COLOR][1] * 255.0 / 100.0) + unmapped_saturation = round( + cast(float, kwargs[ATTR_HS_COLOR][1]) * 255.0 / 100.0 + ) await self.hass.async_add_executor_job( - self.device.set_unmapped_color, (unmapped_hue, unmapped_saturation) + self.data.set_unmapped_color, (unmapped_hue, unmapped_saturation) ) # This will raise 400 BAD REQUEST if the setunmappedcolor is not available except HTTPError as err: @@ -157,18 +160,18 @@ class FritzboxLight(FritzBoxEntity, LightEntity): key=lambda x: abs(x - unmapped_saturation), ) await self.hass.async_add_executor_job( - self.device.set_color, (hue, saturation) + self.data.set_color, (hue, saturation) ) if kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None: await self.hass.async_add_executor_job( - self.device.set_color_temp, kwargs[ATTR_COLOR_TEMP_KELVIN] + self.data.set_color_temp, kwargs[ATTR_COLOR_TEMP_KELVIN] ) - await self.hass.async_add_executor_job(self.device.set_state_on) + await self.hass.async_add_executor_job(self.data.set_state_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - await self.hass.async_add_executor_job(self.device.set_state_off) + await self.hass.async_add_executor_job(self.data.set_state_off) await self.coordinator.async_refresh() diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 422db12b68a..d9b8b1795af 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -1,6 +1,7 @@ { "domain": "fritzbox", "name": "AVM FRITZ!SmartHome", + "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/fritzbox", "requirements": ["pyfritzhome==0.6.7"], "ssdp": [ diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index ab341fb1520..9d68821fdca 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -30,7 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utc_from_timestamp -from . import FritzBoxEntity +from . import FritzBoxDeviceEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN from .model import FritzEntityDescriptionMixinBase @@ -220,14 +220,14 @@ async def async_setup_entry( async_add_entities( [ FritzBoxSensor(coordinator, ain, description) - for ain, device in coordinator.data.items() + for ain, device in coordinator.data.devices.items() for description in SENSOR_TYPES if description.suitable(device) ] ) -class FritzBoxSensor(FritzBoxEntity, SensorEntity): +class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity): """The entity class for FRITZ!SmartHome sensors.""" entity_description: FritzSensorEntityDescription @@ -235,4 +235,4 @@ class FritzBoxSensor(FritzBoxEntity, SensorEntity): @property def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" - return self.entity_description.native_value(self.device) + return self.entity_description.native_value(self.data) diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 79f256bded0..5eee3019633 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FritzBoxEntity +from . import FritzboxDataUpdateCoordinator, FritzBoxDeviceEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN @@ -16,31 +16,33 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the FRITZ!SmartHome switch from ConfigEntry.""" - coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + coordinator: FritzboxDataUpdateCoordinator = hass.data[FRITZBOX_DOMAIN][ + entry.entry_id + ][CONF_COORDINATOR] async_add_entities( [ FritzboxSwitch(coordinator, ain) - for ain, device in coordinator.data.items() + for ain, device in coordinator.data.devices.items() if device.has_switch ] ) -class FritzboxSwitch(FritzBoxEntity, SwitchEntity): +class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): """The switch class for FRITZ!SmartHome switches.""" @property def is_on(self) -> bool: """Return true if the switch is on.""" - return self.device.switch_state # type: ignore [no-any-return] + return self.data.switch_state # type: ignore [no-any-return] async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.hass.async_add_executor_job(self.device.set_switch_state_on) + await self.hass.async_add_executor_job(self.data.set_switch_state_on) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.hass.async_add_executor_job(self.device.set_switch_state_off) + await self.hass.async_add_executor_job(self.data.set_switch_state_off) await self.coordinator.async_refresh() diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index ef2a6083608..167c6fe8de8 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "ignore_ip6_link_local": "IPv6 link local address wird nicht unterst\u00fctzt.", + "ignore_ip6_link_local": "IPv6 Link-Local-Adresse wird nicht unterst\u00fctzt.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "not_supported": "Verbunden mit AVM FRITZ!Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" diff --git a/homeassistant/components/fritzbox/translations/he.json b/homeassistant/components/fritzbox/translations/he.json index ec9248b5ea6..c2c0d4be015 100644 --- a/homeassistant/components/fritzbox/translations/he.json +++ b/homeassistant/components/fritzbox/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { diff --git a/homeassistant/components/fritzbox/translations/sk.json b/homeassistant/components/fritzbox/translations/sk.json index e0e6b1c5bda..c7f02499196 100644 --- a/homeassistant/components/fritzbox/translations/sk.json +++ b/homeassistant/components/fritzbox/translations/sk.json @@ -1,11 +1,40 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "ignore_ip6_link_local": "Lok\u00e1lna adresa odkazu IPv6 nie je podporovan\u00e1.", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Pripojen\u00e9 k AVM FRITZ!Box, ale nedok\u00e1\u017ee ovl\u00e1da\u0165 zariadenia Smart Home.", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Chcete nastavi\u0165 {name}?" + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Aktualizujte svoje prihlasovacie \u00fadaje pre {name}." + }, + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte inform\u00e1cie o va\u0161om AVM FRITZ!Box." + } } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/he.json b/homeassistant/components/fritzbox_callmonitor/translations/he.json index 7951a71054c..fc8b733cb1a 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/he.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" diff --git a/homeassistant/components/fritzbox_callmonitor/translations/sk.json b/homeassistant/components/fritzbox_callmonitor/translations/sk.json index 1145b3bb9f8..632d11fbc78 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/sk.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/sk.json @@ -1,12 +1,25 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, + "flow_title": "{name}", "step": { + "phonebook": { + "data": { + "phonebook": "Telef\u00f3nny zoznam" + } + }, "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/fronius/translations/ru.json b/homeassistant/components/fronius/translations/ru.json index 473834c8797..12d0e012083 100644 --- a/homeassistant/components/fronius/translations/ru.json +++ b/homeassistant/components/fronius/translations/ru.json @@ -2,7 +2,7 @@ "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.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -17,7 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Fronius.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Fronius.", "title": "Fronius SolarNet" } } diff --git a/homeassistant/components/fronius/translations/sk.json b/homeassistant/components/fronius/translations/sk.json new file mode 100644 index 00000000000..7ff40710d48 --- /dev/null +++ b/homeassistant/components/fronius/translations/sk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{device}", + "step": { + "confirm_discovery": { + "description": "Chcete do Home Assistant-a prida\u0165 {device}?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Nakonfigurujte IP adresu alebo miestny n\u00e1zov hostite\u013ea v\u00e1\u0161ho zariadenia Fronius." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ec7001006b1..83514976793 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==20221108.0"], + "requirements": ["home-assistant-frontend==20221207.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 018850f5960..82f169dc6c9 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -9,20 +9,47 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store DATA_STORAGE = "frontend_storage" STORAGE_VERSION_USER_DATA = 1 +@callback +def _initialize_frontend_storage(hass: HomeAssistant) -> None: + """Set up frontend storage.""" + if DATA_STORAGE in hass.data: + return + hass.data[DATA_STORAGE] = ({}, {}) + + async def async_setup_frontend_storage(hass: HomeAssistant) -> None: """Set up frontend storage.""" - hass.data[DATA_STORAGE] = ({}, {}) + _initialize_frontend_storage(hass) websocket_api.async_register_command(hass, websocket_set_user_data) websocket_api.async_register_command(hass, websocket_get_user_data) +async def async_user_store( + hass: HomeAssistant, user_id: str +) -> tuple[Store, dict[str, Any]]: + """Access a user store.""" + _initialize_frontend_storage(hass) + stores, data = hass.data[DATA_STORAGE] + if (store := stores.get(user_id)) is None: + store = stores[user_id] = Store( + hass, + STORAGE_VERSION_USER_DATA, + f"frontend.user_data_{user_id}", + ) + + if user_id not in data: + data[user_id] = await store.async_load() or {} + + return store, data[user_id] + + def with_store(orig_func: Callable) -> Callable: """Decorate function to provide data.""" @@ -31,20 +58,11 @@ def with_store(orig_func: Callable) -> Callable: hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: """Provide user specific data and store to function.""" - stores, data = hass.data[DATA_STORAGE] user_id = connection.user.id - if (store := stores.get(user_id)) is None: - store = stores[user_id] = Store( - hass, - STORAGE_VERSION_USER_DATA, - f"frontend.user_data_{connection.user.id}", - ) + store, user_data = await async_user_store(hass, user_id) - if user_id not in data: - data[user_id] = await store.async_load() or {} - - await orig_func(hass, connection, msg, store, data[user_id]) + await orig_func(hass, connection, msg, store, user_data) return with_store_func diff --git a/homeassistant/components/frontier_silicon/browse_media.py b/homeassistant/components/frontier_silicon/browse_media.py new file mode 100644 index 00000000000..53deb399b87 --- /dev/null +++ b/homeassistant/components/frontier_silicon/browse_media.py @@ -0,0 +1,155 @@ +"""Support for media browsing.""" +import logging + +from afsapi import AFSAPI, FSApiException, OutOfRangeException, Preset + +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, +) + +from .const import MEDIA_CONTENT_ID_CHANNELS, MEDIA_CONTENT_ID_PRESET + +TOP_LEVEL_DIRECTORIES = { + MEDIA_CONTENT_ID_CHANNELS: "Channels", + MEDIA_CONTENT_ID_PRESET: "Presets", +} + +FSAPI_ITEM_TYPE_TO_MEDIA_CLASS = { + 0: MediaClass.DIRECTORY, + 1: MediaClass.CHANNEL, + 2: MediaClass.CHANNEL, +} + +_LOGGER = logging.getLogger(__name__) + + +def _item_preset_payload(preset: Preset, player_mode: str) -> BrowseMedia: + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + return BrowseMedia( + title=preset.name, + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.CHANNEL, + # We add 1 to the preset key to keep it in sync with the numbering shown + # on the interface of the device + media_content_id=f"{player_mode}/{MEDIA_CONTENT_ID_PRESET}/{int(preset.key)+1}", + can_play=True, + can_expand=False, + ) + + +def _item_payload( + key, item: dict[str, str], player_mode: str, parent_keys: list[str] +) -> BrowseMedia: + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + assert "label" in item or "name" in item + assert "type" in item + + title = item.get("label") or item.get("name") or "Unknown" + title = title.strip() + + media_content_id = "/".join( + [player_mode, MEDIA_CONTENT_ID_CHANNELS, *parent_keys, key] + ) + media_class = ( + FSAPI_ITEM_TYPE_TO_MEDIA_CLASS.get(int(item["type"])) or MediaClass.CHANNEL + ) + + return BrowseMedia( + title=title, + media_class=media_class, + media_content_type=MediaClass.CHANNEL, + media_content_id=media_content_id, + can_play=(media_class != MediaClass.DIRECTORY), + can_expand=(media_class == MediaClass.DIRECTORY), + ) + + +async def browse_top_level(current_mode, afsapi: AFSAPI): + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + + children = [ + BrowseMedia( + title=name, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.CHANNELS, + media_content_id=f"{current_mode or 'unknown'}/{top_level_media_content_id}", + can_play=False, + can_expand=True, + ) + for top_level_media_content_id, name in TOP_LEVEL_DIRECTORIES.items() + ] + + library_info = BrowseMedia( + media_class=MediaClass.DIRECTORY, + media_content_id="library", + media_content_type=MediaType.CHANNELS, + title="Media Library", + can_play=False, + can_expand=True, + children=children, + children_media_class=MediaClass.DIRECTORY, + ) + + return library_info + + +async def browse_node( + afsapi: AFSAPI, + media_content_type, + media_content_id, +): + """List the contents of a navigation directory (or preset list).""" + + player_mode, browse_type, *parent_keys = media_content_id.split("/") + + title = TOP_LEVEL_DIRECTORIES.get(browse_type, "Unknown") + + children = [] + try: + if browse_type == MEDIA_CONTENT_ID_PRESET: + # Return the presets + + children = [ + _item_preset_payload(preset, player_mode=player_mode) + for preset in await afsapi.get_presets() + ] + + else: + # Browse to correct folder + await afsapi.nav_select_folder_via_path(parent_keys) + + # Return items in this folder + children = [ + _item_payload(key, item, player_mode, parent_keys=parent_keys) + async for key, item in await afsapi.nav_list() + ] + except OutOfRangeException as err: + raise BrowseError("The requested item is out of range") from err + except FSApiException as err: + raise BrowseError(str(err)) from err + + return BrowseMedia( + title=title, + media_content_id=media_content_id, + media_content_type=MediaType.CHANNELS, + media_class=MediaClass.DIRECTORY, + can_play=False, + can_expand=True, + children=children, + children_media_class=MediaType.CHANNEL, + ) diff --git a/homeassistant/components/frontier_silicon/const.py b/homeassistant/components/frontier_silicon/const.py index 4638e63c2f2..9ee17c0320e 100644 --- a/homeassistant/components/frontier_silicon/const.py +++ b/homeassistant/components/frontier_silicon/const.py @@ -1,6 +1,8 @@ """Constants for the Frontier Silicon Media Player integration.""" - DOMAIN = "frontier_silicon" DEFAULT_PIN = "1234" DEFAULT_PORT = 80 + +MEDIA_CONTENT_ID_PRESET = "preset" +MEDIA_CONTENT_ID_CHANNELS = "channels" diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 309074c1b26..0e3eb168484 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from afsapi import ( AFSAPI, @@ -13,6 +14,8 @@ import voluptuous as vol from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseError, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -25,7 +28,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN +from .browse_media import browse_node, browse_top_level +from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET _LOGGER = logging.getLogger(__name__) @@ -80,7 +84,7 @@ async def async_setup_platform( class AFSAPIDevice(MediaPlayerEntity): """Representation of a Frontier Silicon device on the network.""" - _attr_media_content_type: str = MediaType.MUSIC + _attr_media_content_type: str = MediaType.CHANNEL _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -97,6 +101,7 @@ class AFSAPIDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOUND_MODE + | MediaPlayerEntityFeature.BROWSE_MEDIA ) def __init__(self, name: str | None, afsapi: AFSAPI) -> None: @@ -298,3 +303,42 @@ class AFSAPIDevice(MediaPlayerEntity): and (mode := self.__sound_modes_by_label.get(sound_mode)) is not None ): await self.fs_device.set_eq_preset(mode) + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: + """Browse media library and preset stations.""" + if not media_content_id: + return await browse_top_level(self._attr_source, self.fs_device) + + return await browse_node(self.fs_device, media_content_type, media_content_id) + + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play selected media or channel.""" + if media_type != MediaType.CHANNEL: + _LOGGER.error( + "Got %s, but frontier_silicon only supports playing channels", + media_type, + ) + return + + player_mode, media_type, *keys = media_id.split("/") + + await self.async_select_source(player_mode) # this also powers on the device + + if media_type == MEDIA_CONTENT_ID_PRESET: + if len(keys) != 1: + raise BrowseError("Presets can only have 1 level") + + # Keys of presets are 0-based, while the list shown on the device starts from 1 + preset = int(keys[0]) - 1 + + result = await self.fs_device.select_preset(preset) + else: + result = await self.fs_device.nav_select_item_via_path(keys) + + await self.async_update() + self._attr_media_content_id = media_id + return result diff --git a/homeassistant/components/fully_kiosk/translations/bg.json b/homeassistant/components/fully_kiosk/translations/bg.json index 8dbd96c7099..b25e4f02de1 100644 --- a/homeassistant/components/fully_kiosk/translations/bg.json +++ b/homeassistant/components/fully_kiosk/translations/bg.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/ca.json b/homeassistant/components/fully_kiosk/translations/ca.json index 2cb3945ed01..a0199949870 100644 --- a/homeassistant/components/fully_kiosk/translations/ca.json +++ b/homeassistant/components/fully_kiosk/translations/ca.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/cs.json b/homeassistant/components/fully_kiosk/translations/cs.json index 737979c68e8..b6786dfc3ef 100644 --- a/homeassistant/components/fully_kiosk/translations/cs.json +++ b/homeassistant/components/fully_kiosk/translations/cs.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/de.json b/homeassistant/components/fully_kiosk/translations/de.json index cd099ee8ef9..b20b20a08e3 100644 --- a/homeassistant/components/fully_kiosk/translations/de.json +++ b/homeassistant/components/fully_kiosk/translations/de.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/el.json b/homeassistant/components/fully_kiosk/translations/el.json index 9af93e0c36a..10025a5ca12 100644 --- a/homeassistant/components/fully_kiosk/translations/el.json +++ b/homeassistant/components/fully_kiosk/translations/el.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/en.json b/homeassistant/components/fully_kiosk/translations/en.json index 338c50514fb..24823d68a60 100644 --- a/homeassistant/components/fully_kiosk/translations/en.json +++ b/homeassistant/components/fully_kiosk/translations/en.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/es.json b/homeassistant/components/fully_kiosk/translations/es.json index 1b617892060..6b878ebe6f8 100644 --- a/homeassistant/components/fully_kiosk/translations/es.json +++ b/homeassistant/components/fully_kiosk/translations/es.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/et.json b/homeassistant/components/fully_kiosk/translations/et.json index b046deb0527..b586d35aafb 100644 --- a/homeassistant/components/fully_kiosk/translations/et.json +++ b/homeassistant/components/fully_kiosk/translations/et.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamine nurjus", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/fr.json b/homeassistant/components/fully_kiosk/translations/fr.json index a4a0a822a88..a668af67773 100644 --- a/homeassistant/components/fully_kiosk/translations/fr.json +++ b/homeassistant/components/fully_kiosk/translations/fr.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/he.json b/homeassistant/components/fully_kiosk/translations/he.json index 2a59e340e54..85cb9d13979 100644 --- a/homeassistant/components/fully_kiosk/translations/he.json +++ b/homeassistant/components/fully_kiosk/translations/he.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/hu.json b/homeassistant/components/fully_kiosk/translations/hu.json index cd08f258be2..722d4459786 100644 --- a/homeassistant/components/fully_kiosk/translations/hu.json +++ b/homeassistant/components/fully_kiosk/translations/hu.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/id.json b/homeassistant/components/fully_kiosk/translations/id.json index d9be1351db4..9696b4ced5e 100644 --- a/homeassistant/components/fully_kiosk/translations/id.json +++ b/homeassistant/components/fully_kiosk/translations/id.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/it.json b/homeassistant/components/fully_kiosk/translations/it.json index f8b414166c9..2e4fce4c0d0 100644 --- a/homeassistant/components/fully_kiosk/translations/it.json +++ b/homeassistant/components/fully_kiosk/translations/it.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/ja.json b/homeassistant/components/fully_kiosk/translations/ja.json index b5ef5895312..0cefeae46ba 100644 --- a/homeassistant/components/fully_kiosk/translations/ja.json +++ b/homeassistant/components/fully_kiosk/translations/ja.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/nl.json b/homeassistant/components/fully_kiosk/translations/nl.json index 359990b3e69..4262578285b 100644 --- a/homeassistant/components/fully_kiosk/translations/nl.json +++ b/homeassistant/components/fully_kiosk/translations/nl.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/no.json b/homeassistant/components/fully_kiosk/translations/no.json index 3234879412b..8fa6faa2a15 100644 --- a/homeassistant/components/fully_kiosk/translations/no.json +++ b/homeassistant/components/fully_kiosk/translations/no.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/pl.json b/homeassistant/components/fully_kiosk/translations/pl.json index 905cdeb6149..d74d269219b 100644 --- a/homeassistant/components/fully_kiosk/translations/pl.json +++ b/homeassistant/components/fully_kiosk/translations/pl.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/pt-BR.json b/homeassistant/components/fully_kiosk/translations/pt-BR.json index 2649409ede7..de6f3a24ee9 100644 --- a/homeassistant/components/fully_kiosk/translations/pt-BR.json +++ b/homeassistant/components/fully_kiosk/translations/pt-BR.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Falha ao conectar", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/ru.json b/homeassistant/components/fully_kiosk/translations/ru.json index 00a9a3616ff..80783663c09 100644 --- a/homeassistant/components/fully_kiosk/translations/ru.json +++ b/homeassistant/components/fully_kiosk/translations/ru.json @@ -5,7 +5,6 @@ }, "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/sk.json b/homeassistant/components/fully_kiosk/translations/sk.json new file mode 100644 index 00000000000..74dc2c0fc8e --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/sv.json b/homeassistant/components/fully_kiosk/translations/sv.json index 3aaa8ccb3aa..96e41fdcac2 100644 --- a/homeassistant/components/fully_kiosk/translations/sv.json +++ b/homeassistant/components/fully_kiosk/translations/sv.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/tr.json b/homeassistant/components/fully_kiosk/translations/tr.json index 5d1e2c90e1b..e9130af340b 100644 --- a/homeassistant/components/fully_kiosk/translations/tr.json +++ b/homeassistant/components/fully_kiosk/translations/tr.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/fully_kiosk/translations/zh-Hant.json b/homeassistant/components/fully_kiosk/translations/zh-Hant.json index 5b13923ac70..f2add6e2da6 100644 --- a/homeassistant/components/fully_kiosk/translations/zh-Hant.json +++ b/homeassistant/components/fully_kiosk/translations/zh-Hant.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/garages_amsterdam/translations/sk.json b/homeassistant/components/garages_amsterdam/translations/sk.json new file mode 100644 index 00000000000..30b5d246821 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "garage_name": "N\u00e1zov gar\u00e1\u017ee" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json index 57c275f2beb..b378368a326 100644 --- a/homeassistant/components/gdacs/manifest.json +++ b/homeassistant/components/gdacs/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aio_georss_gdacs==0.7"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "integration_type": "service" } diff --git a/homeassistant/components/gdacs/translations/sk.json b/homeassistant/components/gdacs/translations/sk.json new file mode 100644 index 00000000000..f04d4a327f4 --- /dev/null +++ b/homeassistant/components/gdacs/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 961d3cecfb7..b039b32d73d 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -165,9 +165,8 @@ class GenericCamera(Camera): self._stream_source.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE] - self._attr_supported_features = ( - CameraEntityFeature.STREAM if self._stream_source else 0 - ) + if self._stream_source: + self._attr_supported_features = CameraEntityFeature.STREAM self.content_type = device_info[CONF_CONTENT_TYPE] self.verify_ssl = device_info[CONF_VERIFY_SSL] if device_info.get(CONF_RTSP_TRANSPORT): diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 09f52705734..6fa01ba369e 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -16,7 +16,11 @@ from httpx import HTTPStatusError, RequestError, TimeoutException import voluptuous as vol import yarl -from homeassistant.components.camera import CAMERA_IMAGE_TIMEOUT, _async_get_image +from homeassistant.components.camera import ( + CAMERA_IMAGE_TIMEOUT, + DynamicStreamSettings, + _async_get_image, +) from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, @@ -246,7 +250,13 @@ async def async_test_stream( url = url.with_user(username).with_password(password) stream_source = str(url) try: - stream = create_stream(hass, stream_source, stream_options, "test_stream") + stream = create_stream( + hass, + stream_source, + stream_options, + DynamicStreamSettings(), + "test_stream", + ) hls_provider = stream.add_provider(HLS_PROVIDER) await stream.start() if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT): diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 83b34f73dc8..938e685dcab 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["ha-av==10.0.0", "pillow==9.2.0"], + "requirements": ["ha-av==10.0.0", "pillow==9.3.0"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 7c7e44a67e4..5fddd2d78fe 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -8,8 +8,6 @@ "invalid_still_image": "URL did not return a valid still image", "malformed_url": "Malformed URL", "relative_url": "Relative URLs are not allowed", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "stream_no_route_to_host": "Could not find host while trying to connect to stream", @@ -34,12 +32,6 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } }, - "content_type": { - "description": "Specify the content type for the stream.", - "data": { - "content_type": "Content Type" - } - }, "user_confirm_still": { "title": "Preview", "description": "![Camera Still Image Preview]({preview_url})", @@ -68,12 +60,6 @@ "use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras" } }, - "content_type": { - "description": "[%key:component::generic::config::step::content_type::description%]", - "data": { - "content_type": "[%key:component::generic::config::step::content_type::data::content_type%]" - } - }, "confirm_still": { "title": "[%key:component::generic::config::step::user_confirm_still::title%]", "description": "[%key:component::generic::config::step::user_confirm_still::description%]", diff --git a/homeassistant/components/generic/translations/bg.json b/homeassistant/components/generic/translations/bg.json index 8c6944af94a..1a93acfc2da 100644 --- a/homeassistant/components/generic/translations/bg.json +++ b/homeassistant/components/generic/translations/bg.json @@ -1,29 +1,22 @@ { "config": { "abort": { - "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { + "no_still_image_or_stream_url": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u043f\u043e\u043d\u0435 URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { - "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" - }, - "content_type": { - "data": { - "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435" - }, - "description": "\u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0442\u0438\u043f\u0430 \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435 \u0437\u0430 \u043f\u043e\u0442\u043e\u043a\u0430." - }, "user": { "data": { "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "still_image_url": "URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 (\u043d\u0430\u043f\u0440. http://...)", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - } + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u043a\u0430\u043c\u0435\u0440\u0430\u0442\u0430." }, "user_confirm_still": { "data": { @@ -42,14 +35,9 @@ "data": { "confirmed_ok": "\u0422\u043e\u0432\u0430 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438\u0437\u0433\u043b\u0435\u0436\u0434\u0430 \u0434\u043e\u0431\u0440\u0435." }, + "description": "![\u041f\u0440\u0435\u0433\u043b\u0435\u0434 \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043e\u0442 \u043a\u0430\u043c\u0435\u0440\u0430\u0442\u0430]({preview_url})", "title": "\u041f\u0440\u0435\u0433\u043b\u0435\u0434" }, - "content_type": { - "data": { - "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435" - }, - "description": "\u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0442\u0438\u043f\u0430 \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435 \u0437\u0430 \u043f\u043e\u0442\u043e\u043a\u0430." - }, "init": { "data": { "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 8a03666919e..38da6c6a2cd 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", "relative_url": "Els URL relatius no s'admeten", - "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", - "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", "stream_no_route_to_host": "No s'ha pogut trobar l'amfitri\u00f3 mentre intentava connectar al flux de dades", - "stream_no_video": "El flux no cont\u00e9 v\u00eddeo", "stream_not_permitted": "Operaci\u00f3 no permesa mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", - "stream_unauthorised": "L'autoritzaci\u00f3 ha fallat mentre s'intentava connectar amb el flux de dades", "template_error": "Error renderitzant plantilla. Consulta els registres per m\u00e9s informaci\u00f3.", "timeout": "El temps m\u00e0xim de c\u00e0rrega de l'URL ha expirat", "unable_still_load": "No s'ha pogut carregar cap imatge v\u00e0lida des de l'URL d'imatge fixa (pot ser per un amfitri\u00f3 o URL inv\u00e0lid o un error d'autenticaci\u00f3). Revisa els registres per a m\u00e9s informaci\u00f3.", "unknown": "Error inesperat" }, "step": { - "confirm": { - "description": "Vols comen\u00e7ar la configuraci\u00f3?" - }, - "content_type": { - "data": { - "content_type": "Tipus de contingut" - }, - "description": "Especifica el tipus de contingut per al flux de dades (stream)." - }, "user": { "data": { "authentication": "Autenticaci\u00f3", @@ -62,13 +48,9 @@ "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", "relative_url": "Els URL relatius no s'admeten", - "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", - "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", "stream_no_route_to_host": "No s'ha pogut trobar l'amfitri\u00f3 mentre intentava connectar al flux de dades", - "stream_no_video": "El flux no cont\u00e9 v\u00eddeo", "stream_not_permitted": "Operaci\u00f3 no permesa mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", - "stream_unauthorised": "L'autoritzaci\u00f3 ha fallat mentre s'intentava connectar amb el flux de dades", "template_error": "Error renderitzant plantilla. Consulta els registres per m\u00e9s informaci\u00f3.", "timeout": "El temps m\u00e0xim de c\u00e0rrega de l'URL ha expirat", "unable_still_load": "No s'ha pogut carregar cap imatge v\u00e0lida des de l'URL d'imatge fixa (pot ser per un amfitri\u00f3 o URL inv\u00e0lid o un error d'autenticaci\u00f3). Revisa els registres per a m\u00e9s informaci\u00f3.", @@ -82,12 +64,6 @@ "description": "![Vista pr\u00e8via de la imatge de la c\u00e0mera]({preview_url})", "title": "Vista pr\u00e8via" }, - "content_type": { - "data": { - "content_type": "Tipus de contingut" - }, - "description": "Especifica el tipus de contingut per al flux de dades (stream)." - }, "init": { "data": { "authentication": "Autenticaci\u00f3", diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index f6756ac66a9..4f46944044a 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -1,35 +1,42 @@ { "config": { - "abort": { - "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" - }, "error": { + "already_exists": "Kamera s t\u011bmito nastaven\u00edmi URL ji\u017e existuje.", + "invalid_still_image": "Adresa URL nevr\u00e1tila platn\u00fd statick\u00fd obr\u00e1zek", + "relative_url": "Relativn\u00ed adresy URL nejsou povoleny", + "timeout": "P\u0159i na\u010d\u00edt\u00e1n\u00ed adresy URL vypr\u0161el \u010dasov\u00fd limit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "content_type": { - "data": { - "content_type": "Typ obsahu" - } - }, "user": { "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, "description": "Zadejte nastaven\u00ed pro p\u0159ipojen\u00ed ke kame\u0159e." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Tento obr\u00e1zek vypad\u00e1 dob\u0159e." + }, + "description": "![N\u00e1hled statick\u00e9ho sn\u00edmku z fotoapar\u00e1tu]( {preview_url} )", + "title": "N\u00e1hled" } } }, "options": { "error": { + "already_exists": "Kamera s t\u011bmito nastaven\u00edmi URL ji\u017e existuje.", + "invalid_still_image": "Adresa URL nevr\u00e1tila platn\u00fd statick\u00fd obr\u00e1zek", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "content_type": { + "confirm_still": { "data": { - "content_type": "Typ obsahu" - } + "confirmed_ok": "Tento obr\u00e1zek vypad\u00e1 dob\u0159e." + }, + "description": "![N\u00e1hled statick\u00e9ho sn\u00edmku z fotoapar\u00e1tu]( {preview_url} )", + "title": "N\u00e1hled" }, "init": { "data": { diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index 503f78fea32..d45de3644f4 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", "relative_url": "Relative URLs sind nicht zul\u00e4ssig", - "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", - "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", "stream_no_route_to_host": "Beim Versuch, eine Verbindung zum Stream herzustellen, konnte der Host nicht gefunden werden", - "stream_no_video": "Stream enth\u00e4lt kein Video", "stream_not_permitted": "Beim Versuch, eine Verbindung zum Stream herzustellen, ist ein Vorgang nicht zul\u00e4ssig. Falsches RTSP-Transportprotokoll?", - "stream_unauthorised": "Autorisierung beim Versuch, eine Verbindung zum Stream herzustellen, fehlgeschlagen", "template_error": "Fehler beim Rendern der Vorlage. \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", "timeout": "Zeit\u00fcberschreitung beim Laden der URL", "unable_still_load": "Es konnte kein g\u00fcltiges Bild von der Standbild-URL geladen werden (z. B. ung\u00fcltiger Host, URL oder Authentifizierungsfehler). \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", "unknown": "Unerwarteter Fehler" }, "step": { - "confirm": { - "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" - }, - "content_type": { - "data": { - "content_type": "Inhaltstyp" - }, - "description": "Gib den Inhaltstyp des Streams an." - }, "user": { "data": { "authentication": "Authentifizierung", @@ -62,13 +48,9 @@ "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", "relative_url": "Relative URLs sind nicht zul\u00e4ssig", - "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", - "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", "stream_no_route_to_host": "Beim Versuch, eine Verbindung zum Stream herzustellen, konnte der Host nicht gefunden werden", - "stream_no_video": "Stream enth\u00e4lt kein Video", "stream_not_permitted": "Beim Versuch, eine Verbindung zum Stream herzustellen, ist ein Vorgang nicht zul\u00e4ssig. Falsches RTSP-Transportprotokoll?", - "stream_unauthorised": "Autorisierung beim Versuch, eine Verbindung zum Stream herzustellen, fehlgeschlagen", "template_error": "Fehler beim Rendern der Vorlage. \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", "timeout": "Zeit\u00fcberschreitung beim Laden der URL", "unable_still_load": "Es konnte kein g\u00fcltiges Bild von der Standbild-URL geladen werden (z. B. ung\u00fcltiger Host, URL oder Authentifizierungsfehler). \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", @@ -82,12 +64,6 @@ "description": "![Kamera-Standbildvorschau]({preview_url})", "title": "Vorschau" }, - "content_type": { - "data": { - "content_type": "Inhaltstyp" - }, - "description": "Gib den Inhaltstyp des Streams an." - }, "init": { "data": { "authentication": "Authentifizierung", diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index eda806cc137..6486f13849c 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", - "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", - "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", "stream_no_route_to_host": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", - "stream_no_video": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", "stream_not_permitted": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", - "stream_unauthorised": "\u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "template_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03c4\u03cd\u03c0\u03bf\u03c5. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL", "unable_still_load": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2, \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2). \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { - "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" - }, - "content_type": { - "data": { - "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5" - }, - "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03bf\u03ae." - }, "user": { "data": { "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", @@ -62,24 +48,21 @@ "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", - "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", - "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", "stream_no_route_to_host": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", - "stream_no_video": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", "stream_not_permitted": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", - "stream_unauthorised": "\u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "template_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03c4\u03cd\u03c0\u03bf\u03c5. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL", "unable_still_load": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2, \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2). \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { - "content_type": { + "confirm_still": { "data": { - "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5" + "confirmed_ok": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b1\u03bb\u03ae." }, - "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03bf\u03ae." + "description": "! [\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2] ({preview_url})", + "title": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7" }, "init": { "data": { diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index a9ea9a82d13..1215078ca33 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "relative_url": "Relative URLs are not allowed", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "unknown": "Unexpected error" }, "step": { - "confirm": { - "description": "Do you want to start set up?" - }, - "content_type": { - "data": { - "content_type": "Content Type" - }, - "description": "Specify the content type for the stream." - }, "user": { "data": { "authentication": "Authentication", @@ -62,13 +48,9 @@ "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "relative_url": "Relative URLs are not allowed", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -82,12 +64,6 @@ "description": "![Camera Still Image Preview]({preview_url})", "title": "Preview" }, - "content_type": { - "data": { - "content_type": "Content Type" - }, - "description": "Specify the content type for the stream." - }, "init": { "data": { "authentication": "Authentication", diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index 42362863623..564e0488dfa 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No se encontraron dispositivos en la red", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "URL con formato incorrecto", "no_still_image_or_stream_url": "Debes especificar al menos una imagen fija o URL de transmisi\u00f3n", "relative_url": "No se permiten URLs relativas", - "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", - "stream_http_not_found": "HTTP 404 Not found al intentar conectarse a la transmisi\u00f3n", "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", "stream_no_route_to_host": "No se pudo encontrar el host al intentar conectarse a la transmisi\u00f3n", - "stream_no_video": "La transmisi\u00f3n no tiene video", "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_unauthorised": "La autorizaci\u00f3n fall\u00f3 al intentar conectarse a la transmisi\u00f3n", "template_error": "Error al renderizar la plantilla. Revisa el registro para obtener m\u00e1s informaci\u00f3n.", "timeout": "Tiempo de espera al cargar la URL", "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revisa el registro para obtener m\u00e1s informaci\u00f3n.", "unknown": "Error inesperado" }, "step": { - "confirm": { - "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" - }, - "content_type": { - "data": { - "content_type": "Tipos de contenido" - }, - "description": "Especifica el tipo de contenido para el flujo." - }, "user": { "data": { "authentication": "Autenticaci\u00f3n", @@ -62,13 +48,9 @@ "malformed_url": "URL con formato incorrecto", "no_still_image_or_stream_url": "Debes especificar al menos una imagen fija o URL de transmisi\u00f3n", "relative_url": "No se permiten URLs relativas", - "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", - "stream_http_not_found": "HTTP 404 Not found al intentar conectarse a la transmisi\u00f3n", "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", "stream_no_route_to_host": "No se pudo encontrar el host al intentar conectarse a la transmisi\u00f3n", - "stream_no_video": "La transmisi\u00f3n no tiene video", "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_unauthorised": "La autorizaci\u00f3n fall\u00f3 al intentar conectarse a la transmisi\u00f3n", "template_error": "Error al renderizar la plantilla. Revisa el registro para obtener m\u00e1s informaci\u00f3n.", "timeout": "Tiempo de espera al cargar la URL", "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revisa el registro para obtener m\u00e1s informaci\u00f3n.", @@ -82,12 +64,6 @@ "description": "![Vista previa de imagen fija de c\u00e1mara]({preview_url})", "title": "Vista previa" }, - "content_type": { - "data": { - "content_type": "Tipos de contenido" - }, - "description": "Especifica el tipo de contenido para el flujo." - }, "init": { "data": { "authentication": "Autenticaci\u00f3n", diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index e89e968a2f9..c5f9b045b15 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", "relative_url": "Osalised URL-id pole lubatud", - "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", - "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", "stream_no_route_to_host": "Vooga \u00fchenduse loomisel ei leitud hosti", - "stream_no_video": "Voos pole videot", "stream_not_permitted": "Vooga \u00fchenduse loomisel pole toiming lubatud. Vale RTSP transpordiprotokoll?", - "stream_unauthorised": "Autoriseerimine eba\u00f5nnestus vooga \u00fchendamise ajal", "template_error": "Viga malli renderdamisel. Lisateabe saamiseks vaata logi.", "timeout": "URL-i laadimise ajal\u00f5pp", "unable_still_load": "Pilti ei saa laadida URL-ist (nt kehtetu host, URL v\u00f5i autentimise t\u00f5rge). Lisateabe saamiseks vaata logi.", "unknown": "Ootamatu t\u00f5rge" }, "step": { - "confirm": { - "description": "Kas alustada seadistamist?" - }, - "content_type": { - "data": { - "content_type": "Sisu t\u00fc\u00fcp" - }, - "description": "M\u00e4\u00e4ra voo sisut\u00fc\u00fcp." - }, "user": { "data": { "authentication": "Autentimine", @@ -62,13 +48,9 @@ "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", "relative_url": "Osalised URL-id pole lubatud", - "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", - "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", "stream_no_route_to_host": "Vooga \u00fchenduse loomisel ei leitud hosti", - "stream_no_video": "Voos pole videot", "stream_not_permitted": "Vooga \u00fchenduse loomisel pole toiming lubatud. Vale RTSP transpordiprotokoll?", - "stream_unauthorised": "Autoriseerimine eba\u00f5nnestus vooga \u00fchendamise ajal", "template_error": "Viga malli renderdamisel. Lisateabe saamiseks vaata logi.", "timeout": "URL-i laadimise ajal\u00f5pp", "unable_still_load": "Pilti ei saa laadida URL-ist (nt kehtetu host, URL v\u00f5i autentimise t\u00f5rge). Lisateabe saamiseks vaata logi.", @@ -82,12 +64,6 @@ "description": "![Camera Still Image Preview]({preview_url})", "title": "Eelvaade" }, - "content_type": { - "data": { - "content_type": "Sisu t\u00fc\u00fcp" - }, - "description": "M\u00e4\u00e4ra voo sisut\u00fc\u00fcp." - }, "init": { "data": { "authentication": "Autentimine", diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index d6c057d4da1..538ff8ae1fe 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", - "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", - "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", "stream_no_route_to_host": "Impossible de trouver l'h\u00f4te lors de la tentative de connexion au flux", - "stream_no_video": "Le flux ne contient pas de vid\u00e9o", "stream_not_permitted": "Op\u00e9ration non autoris\u00e9e lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", - "stream_unauthorised": "\u00c9chec de l'autorisation lors de la tentative de connexion au flux", "template_error": "Erreur lors du rendu du mod\u00e8le. Consultez le journal pour plus d'informations.", "timeout": "D\u00e9lai d'attente expir\u00e9 lors du chargement de l'URL", "unable_still_load": "Impossible de charger une image valide depuis l'URL d'image fixe (cela pourrait \u00eatre d\u00fb \u00e0 un h\u00f4te ou \u00e0 une URL non valide, ou \u00e0 un \u00e9chec de l'authentification). Consultez le journal pour plus d'informations.", "unknown": "Erreur inattendue" }, "step": { - "confirm": { - "description": "Voulez-vous commencer la configuration\u00a0?" - }, - "content_type": { - "data": { - "content_type": "Type de contenu" - }, - "description": "Sp\u00e9cifiez le type de contenu du flux." - }, "user": { "data": { "authentication": "Authentification", @@ -47,6 +33,9 @@ "description": "Saisissez les param\u00e8tres de connexion \u00e0 la cam\u00e9ra." }, "user_confirm_still": { + "data": { + "confirmed_ok": "Cette image semble conforme." + }, "title": "Aper\u00e7u" } } @@ -58,13 +47,9 @@ "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", - "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", - "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", "stream_no_route_to_host": "Impossible de trouver l'h\u00f4te lors de la tentative de connexion au flux", - "stream_no_video": "Le flux ne contient pas de vid\u00e9o", "stream_not_permitted": "Op\u00e9ration non autoris\u00e9e lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", - "stream_unauthorised": "\u00c9chec de l'autorisation lors de la tentative de connexion au flux", "template_error": "Erreur lors du rendu du mod\u00e8le. Consultez le journal pour plus d'informations.", "timeout": "D\u00e9lai d'attente expir\u00e9 lors du chargement de l'URL", "unable_still_load": "Impossible de charger une image valide depuis l'URL d'image fixe (cela pourrait \u00eatre d\u00fb \u00e0 un h\u00f4te ou \u00e0 une URL non valide, ou \u00e0 un \u00e9chec de l'authentification). Consultez le journal pour plus d'informations.", @@ -72,13 +57,10 @@ }, "step": { "confirm_still": { - "title": "Aper\u00e7u" - }, - "content_type": { "data": { - "content_type": "Type de contenu" + "confirmed_ok": "Cette image semble conforme." }, - "description": "Sp\u00e9cifiez le type de contenu du flux." + "title": "Aper\u00e7u" }, "init": { "data": { diff --git a/homeassistant/components/generic/translations/he.json b/homeassistant/components/generic/translations/he.json index 2a3458cd3e7..e92df2f384b 100644 --- a/homeassistant/components/generic/translations/he.json +++ b/homeassistant/components/generic/translations/he.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05d2\u05d5\u05d9\u05d4", "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", "relative_url": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d0\u05ea\u05e8\u05d9\u05dd \u05d9\u05d7\u05e1\u05d9\u05d5\u05ea \u05d0\u05d9\u05e0\u05df \u05de\u05d5\u05ea\u05e8\u05d5\u05ea", - "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", - "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", "stream_no_route_to_host": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", - "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", - "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "template_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05d9\u05d1\u05d5\u05d3 \u05d4\u05ea\u05d1\u05e0\u05d9\u05ea. \u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { - "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" - }, - "content_type": { - "data": { - "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df" - }, - "description": "\u05e0\u05d0 \u05dc\u05e6\u05d9\u05d9\u05df \u05d0\u05ea \u05e1\u05d5\u05d2 \u05d4\u05ea\u05d5\u05db\u05df \u05e2\u05d1\u05d5\u05e8 \u05d4\u05d6\u05e8\u05dd." - }, "user": { "data": { "authentication": "\u05d0\u05d9\u05de\u05d5\u05ea", @@ -45,6 +31,13 @@ "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" }, "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05e6\u05dc\u05de\u05d4." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "\u05d4\u05ea\u05de\u05d5\u05e0\u05d4 \u05d4\u05d6\u05d5 \u05e0\u05e8\u05d0\u05d9\u05ea \u05d8\u05d5\u05d1." + }, + "description": "![\u05ea\u05e6\u05d5\u05d2\u05d4 \u05de\u05e7\u05d3\u05d9\u05de\u05d4 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d1\u05de\u05e6\u05dc\u05de\u05d4]({preview_url})", + "title": "\u05ea\u05e6\u05d5\u05d2\u05d4 \u05de\u05e7\u05d3\u05d9\u05de\u05d4" } } }, @@ -55,24 +48,21 @@ "malformed_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05d2\u05d5\u05d9\u05d4", "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", "relative_url": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d0\u05ea\u05e8\u05d9\u05dd \u05d9\u05d7\u05e1\u05d9\u05d5\u05ea \u05d0\u05d9\u05e0\u05df \u05de\u05d5\u05ea\u05e8\u05d5\u05ea", - "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", - "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", "stream_no_route_to_host": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", - "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", - "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "template_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05d9\u05d1\u05d5\u05d3 \u05d4\u05ea\u05d1\u05e0\u05d9\u05ea. \u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { - "content_type": { + "confirm_still": { "data": { - "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df" + "confirmed_ok": "\u05d4\u05ea\u05de\u05d5\u05e0\u05d4 \u05d4\u05d6\u05d5 \u05e0\u05e8\u05d0\u05d9\u05ea \u05d8\u05d5\u05d1." }, - "description": "\u05e0\u05d0 \u05dc\u05e6\u05d9\u05d9\u05df \u05d0\u05ea \u05e1\u05d5\u05d2 \u05d4\u05ea\u05d5\u05db\u05df \u05e2\u05d1\u05d5\u05e8 \u05d4\u05d6\u05e8\u05dd." + "description": "![\u05ea\u05e6\u05d5\u05d2\u05d4 \u05de\u05e7\u05d3\u05d9\u05de\u05d4 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d1\u05de\u05e6\u05dc\u05de\u05d4]({preview_url})", + "title": "\u05ea\u05e6\u05d5\u05d2\u05d4 \u05de\u05e7\u05d3\u05d9\u05de\u05d4" }, "init": { "data": { diff --git a/homeassistant/components/generic/translations/hr.json b/homeassistant/components/generic/translations/hr.json new file mode 100644 index 00000000000..2dd06c640c9 --- /dev/null +++ b/homeassistant/components/generic/translations/hr.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user_confirm_still": { + "data": { + "confirmed_ok": "Ova slika izgleda dobro." + }, + "description": "![Pregled fotografije s kamere]( {preview_url} )", + "title": "Pregled" + } + } + }, + "options": { + "step": { + "confirm_still": { + "data": { + "confirmed_ok": "Ova slika izgleda dobro." + }, + "description": "![Pregled fotografije s kamere]({preview_url})", + "title": "Pregled" + }, + "init": { + "data": { + "password": "Lozinka", + "rtsp_transport": "RTSP transportni protokol", + "username": "Korisni\u010dko ime", + "verify_ssl": "Provjera SSL certifikata" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index a36457999ca..87a292910ab 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", - "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", - "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", "stream_no_route_to_host": "Nem tal\u00e1lhat\u00f3 a c\u00edm, mik\u00f6zben a rendszer az adatfolyamhoz pr\u00f3b\u00e1l csatlakozni", - "stream_no_video": "Az adatfolyamban nincs vide\u00f3", "stream_not_permitted": "A m\u0171velet nem enged\u00e9lyezett, mik\u00f6zben megpr\u00f3b\u00e1l csatlakozni a folyamhoz. Rossz fajta RTSP protokoll?", - "stream_unauthorised": "A hiteles\u00edt\u00e9s meghi\u00fasult, mik\u00f6zben megpr\u00f3b\u00e1lt csatlakozni az adatfolyamhoz", "template_error": "Hiba t\u00f6rt\u00e9nt a sablon renderel\u00e9se k\u00f6zben. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az URL bet\u00f6lt\u00e9se k\u00f6zben", "unable_still_load": "Nem siker\u00fclt \u00e9rv\u00e9nyes k\u00e9pet bet\u00f6lteni az \u00e1ll\u00f3k\u00e9p URL-c\u00edm\u00e9r\u0151l (pl. \u00e9rv\u00e9nytelen host, URL vagy hiteles\u00edt\u00e9si hiba). Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { - "confirm": { - "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" - }, - "content_type": { - "data": { - "content_type": "Tartalom t\u00edpus" - }, - "description": "Az adatfolyam tartalomt\u00edpua (Content-Type)." - }, "user": { "data": { "authentication": "Hiteles\u00edt\u00e9s", @@ -50,7 +36,7 @@ "data": { "confirmed_ok": "A k\u00e9p megfelel\u0151" }, - "description": "![Kamerak\u00e9p el\u0151n\u00e9zet] ({preview_url})", + "description": "![Kamerak\u00e9p el\u0151n\u00e9zet]({preview_url})", "title": "El\u0151n\u00e9zet" } } @@ -62,24 +48,21 @@ "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", - "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", - "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", "stream_no_route_to_host": "Nem tal\u00e1lhat\u00f3 a c\u00edm, mik\u00f6zben a rendszer az adatfolyamhoz pr\u00f3b\u00e1l csatlakozni", - "stream_no_video": "Az adatfolyamban nincs vide\u00f3", "stream_not_permitted": "A m\u0171velet nem enged\u00e9lyezett, mik\u00f6zben megpr\u00f3b\u00e1l csatlakozni a folyamhoz. Rossz fajta RTSP protokoll?", - "stream_unauthorised": "A hiteles\u00edt\u00e9s meghi\u00fasult, mik\u00f6zben megpr\u00f3b\u00e1lt csatlakozni az adatfolyamhoz", "template_error": "Hiba t\u00f6rt\u00e9nt a sablon renderel\u00e9se k\u00f6zben. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az URL bet\u00f6lt\u00e9se k\u00f6zben", "unable_still_load": "Nem siker\u00fclt \u00e9rv\u00e9nyes k\u00e9pet bet\u00f6lteni az \u00e1ll\u00f3k\u00e9p URL-c\u00edm\u00e9r\u0151l (pl. \u00e9rv\u00e9nytelen host, URL vagy hiteles\u00edt\u00e9si hiba). Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { - "content_type": { + "confirm_still": { "data": { - "content_type": "Tartalom t\u00edpus" + "confirmed_ok": "A k\u00e9p megfelel\u0151" }, - "description": "Az adatfolyam tartalomt\u00edpua (Content-Type)." + "description": "![Kamerak\u00e9p el\u0151n\u00e9zet]({preview_url})", + "title": "El\u0151n\u00e9zet" }, "init": { "data": { diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 8cc0ca6aefc..7843f58e7a4 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", "relative_url": "URL relatif tidak diizinkan", - "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", - "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", - "stream_no_video": "Streaming tidak memiliki video", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", - "stream_unauthorised": "Otorisasi gagal saat mencoba menyambung ke streaming", "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", "timeout": "Tenggang waktu habis saat memuat URL", "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { - "confirm": { - "description": "Ingin memulai penyiapan?" - }, - "content_type": { - "data": { - "content_type": "Jenis Konten" - }, - "description": "Tentukan jenis konten untuk streaming." - }, "user": { "data": { "authentication": "Autentikasi", @@ -62,24 +48,21 @@ "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", "relative_url": "URL relatif tidak diizinkan", - "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", - "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", - "stream_no_video": "Streaming tidak memiliki video", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", - "stream_unauthorised": "Otorisasi gagal saat mencoba menyambung ke streaming", "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", "timeout": "Tenggang waktu habis saat memuat URL", "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { - "content_type": { + "confirm_still": { "data": { - "content_type": "Jenis Konten" + "confirmed_ok": "Gambar ini terlihat bagus." }, - "description": "Tentukan jenis konten untuk streaming." + "description": "![Pratinjau Gambar Diam Kamera]({preview_url})", + "title": "Pratinjau" }, "init": { "data": { diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 0fd99c649a1..3337d1e4552 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", "relative_url": "Non sono consentiti URL relativi", - "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", - "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", "stream_no_route_to_host": "Impossibile trovare l'host durante il tentativo di connessione al flusso", - "stream_no_video": "Il flusso non ha video", "stream_not_permitted": "Operazione non consentita durante il tentativo di connessione al . Protocollo di trasporto RTSP errato?", - "stream_unauthorised": "Autorizzazione non riuscita durante il tentativo di connessione al flusso", "template_error": "Errore durante l'esecuzione del modello. Esamina il registro per ulteriori informazioni.", "timeout": "Timeout durante il caricamento dell'URL", "unable_still_load": "Impossibile caricare un'immagine valida dall'URL dell'immagine fissa (ad es. host, URL non valido o errore di autenticazione). Esamina il registro per ulteriori informazioni.", "unknown": "Errore imprevisto" }, "step": { - "confirm": { - "description": "Vuoi iniziare la configurazione?" - }, - "content_type": { - "data": { - "content_type": "Tipo di contenuto" - }, - "description": "Specificare il tipo di contenuto per il flusso." - }, "user": { "data": { "authentication": "Autenticazione", @@ -50,7 +36,7 @@ "data": { "confirmed_ok": "Questa immagine sembra buona." }, - "description": "![Anteprima immagine fissa fotocamera]({preview_url})", + "description": "![Anteprima immagine fissa della fotocamera]({preview_url})", "title": "Anteprima" } } @@ -62,13 +48,9 @@ "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", "relative_url": "Non sono consentiti URL relativi", - "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", - "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", "stream_no_route_to_host": "Impossibile trovare l'host durante il tentativo di connessione al flusso", - "stream_no_video": "Il flusso non ha video", "stream_not_permitted": "Operazione non consentita durante il tentativo di connessione al . Protocollo di trasporto RTSP errato?", - "stream_unauthorised": "Autorizzazione non riuscita durante il tentativo di connessione al flusso", "template_error": "Errore durante l'esecuzione del modello. Esamina il registro per ulteriori informazioni.", "timeout": "Timeout durante il caricamento dell'URL", "unable_still_load": "Impossibile caricare un'immagine valida dall'URL dell'immagine fissa (ad es. host, URL non valido o errore di autenticazione). Esamina il registro per ulteriori informazioni.", @@ -77,17 +59,11 @@ "step": { "confirm_still": { "data": { - "confirmed_ok": "Questa immagine appare bene" + "confirmed_ok": "Questa immagine sembra buona." }, - "description": "![Anteprima dell' immagine fissa della fotocamera]({preview_url})", + "description": "![Anteprima immagine fissa della fotocamera]({preview_url})", "title": "Anteprima" }, - "content_type": { - "data": { - "content_type": "Tipo di contenuto" - }, - "description": "Specificare il tipo di contenuto per il flusso." - }, "init": { "data": { "authentication": "Autenticazione", diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index f07da6e04fc..9f06c618c3a 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", - "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", - "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", "stream_no_route_to_host": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u307e\u3057\u305f\u304c\u3001\u30db\u30b9\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", - "stream_no_video": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u52d5\u753b\u304c\u3042\u308a\u307e\u305b\u3093", "stream_not_permitted": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u9593\u3001\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", - "stream_unauthorised": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "template_error": "\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u306e\u30ec\u30f3\u30c0\u30ea\u30f3\u30b0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "timeout": "URL\u306e\u8aad\u307f\u8fbc\u307f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "unable_still_load": "\u9759\u6b62\u753b\u306eURL\u304b\u3089\u6709\u52b9\u306a\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u3080\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\uff08\u4f8b: \u7121\u52b9\u306a\u30db\u30b9\u30c8\u3001URL\u3001\u307e\u305f\u306f\u8a8d\u8a3c\u5931\u6557)\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { - "confirm": { - "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" - }, - "content_type": { - "data": { - "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e" - }, - "description": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bf\u30a4\u30d7\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002" - }, "user": { "data": { "authentication": "\u8a8d\u8a3c", @@ -58,25 +44,15 @@ "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", - "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", - "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", "stream_no_route_to_host": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u307e\u3057\u305f\u304c\u3001\u30db\u30b9\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", - "stream_no_video": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u52d5\u753b\u304c\u3042\u308a\u307e\u305b\u3093", "stream_not_permitted": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u9593\u3001\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", - "stream_unauthorised": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "template_error": "\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u306e\u30ec\u30f3\u30c0\u30ea\u30f3\u30b0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "timeout": "URL\u306e\u8aad\u307f\u8fbc\u307f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "unable_still_load": "\u9759\u6b62\u753b\u306eURL\u304b\u3089\u6709\u52b9\u306a\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u3080\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\uff08\u4f8b: \u7121\u52b9\u306a\u30db\u30b9\u30c8\u3001URL\u3001\u307e\u305f\u306f\u8a8d\u8a3c\u5931\u6557)\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { - "content_type": { - "data": { - "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e" - }, - "description": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bf\u30a4\u30d7\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002" - }, "init": { "data": { "authentication": "\u8a8d\u8a3c", diff --git a/homeassistant/components/generic/translations/ko.json b/homeassistant/components/generic/translations/ko.json deleted file mode 100644 index 20ad990e862..00000000000 --- a/homeassistant/components/generic/translations/ko.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "confirm": { - "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json index b7727190810..d6d1d380990 100644 --- a/homeassistant/components/generic/translations/nl.json +++ b/homeassistant/components/generic/translations/nl.json @@ -1,35 +1,21 @@ { "config": { "abort": { - "no_devices_found": "Geen apparaten gevonden op het netwerk", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "already_exists": "Een camera met deze URL instellingen bestaat al.", "invalid_still_image": "URL heeft geen geldig stilstaand beeld geretourneerd", "no_still_image_or_stream_url": "U moet ten minste een stilstaand beeld of stream-URL specificeren", - "stream_file_not_found": "Bestand niet gevonden tijdens verbinding met stream (is ffmpeg ge\u00efnstalleerd?)", - "stream_http_not_found": "HTTP 404 Niet gevonden bij poging om verbinding te maken met stream", "stream_io_error": "Input/Output fout bij het proberen te verbinden met stream. Verkeerde RTSP transport protocol?", "stream_no_route_to_host": "Kan de host niet vinden terwijl u verbinding probeert te maken met de stream", - "stream_no_video": "Stream heeft geen video", "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", - "stream_unauthorised": "Autorisatie mislukt bij poging om verbinding te maken met stream", "template_error": "Fout bij het weergeven van sjabloon. Bekijk het logboek voor meer informatie.", "timeout": "Time-out tijdens het laden van URL", "unable_still_load": "Kan geen geldige afbeelding laden van stilstaande afbeelding URL (b.v. ongeldige host, URL of authenticatie fout). Bekijk het log voor meer informatie.", "unknown": "Onverwachte fout" }, "step": { - "confirm": { - "description": "Wil je beginnen met instellen?" - }, - "content_type": { - "data": { - "content_type": "Inhoudstype" - }, - "description": "Geef het inhoudstype voor de stream op." - }, "user": { "data": { "authentication": "Authenticatie", @@ -51,25 +37,15 @@ "already_exists": "Een camera met deze URL instellingen bestaat al.", "invalid_still_image": "URL heeft geen geldig stilstaand beeld geretourneerd", "no_still_image_or_stream_url": "U moet ten minste een stilstaand beeld of stream-URL specificeren", - "stream_file_not_found": "Bestand niet gevonden tijdens verbinding met stream (is ffmpeg ge\u00efnstalleerd?)", - "stream_http_not_found": "HTTP 404 Niet gevonden bij poging om verbinding te maken met stream", "stream_io_error": "Input/Output fout bij het proberen te verbinden met stream. Verkeerde RTSP transport protocol?", "stream_no_route_to_host": "Kan de host niet vinden terwijl u verbinding probeert te maken met de stream", - "stream_no_video": "Stream heeft geen video", "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", - "stream_unauthorised": "Autorisatie mislukt bij poging om verbinding te maken met stream", "template_error": "Fout bij het weergeven van sjabloon. Bekijk het logboek voor meer informatie.", "timeout": "Time-out tijdens het laden van URL", "unable_still_load": "Kan geen geldige afbeelding laden van stilstaande afbeelding URL (b.v. ongeldige host, URL of authenticatie fout). Bekijk het log voor meer informatie.", "unknown": "Onverwachte fout" }, "step": { - "content_type": { - "data": { - "content_type": "Inhoudstype" - }, - "description": "Geef het inhoudstype voor de stream op." - }, "init": { "data": { "authentication": "Authenticatie", diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index a4c9c27bf69..0960b781ea9 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", "relative_url": "Relative URL-adresser ikke tillatt", - "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", - "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", "stream_no_route_to_host": "Kunne ikke finne verten under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", - "stream_no_video": "Stream har ingen video", "stream_not_permitted": "Operasjon er ikke tillatt mens du pr\u00f8ver \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", - "stream_unauthorised": "Autorisasjonen mislyktes under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", "template_error": "Feil ved gjengivelse av mal. Se gjennom loggen for mer informasjon.", "timeout": "Tidsavbrudd under innlasting av URL", "unable_still_load": "Kan ikke laste inn gyldig bilde fra URL-adresse for stillbilde (f.eks. ugyldig verts-, URL- eller godkjenningsfeil). Se gjennom loggen hvis du vil ha mer informasjon.", "unknown": "Uventet feil" }, "step": { - "confirm": { - "description": "Vil du starte oppsettet?" - }, - "content_type": { - "data": { - "content_type": "Innholdstype" - }, - "description": "Angi innholdstypen for str\u00f8mmen." - }, "user": { "data": { "authentication": "Godkjenning", @@ -62,13 +48,9 @@ "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", "relative_url": "Relative URL-adresser ikke tillatt", - "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", - "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", "stream_no_route_to_host": "Kunne ikke finne verten under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", - "stream_no_video": "Stream har ingen video", "stream_not_permitted": "Operasjon er ikke tillatt mens du pr\u00f8ver \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", - "stream_unauthorised": "Autorisasjonen mislyktes under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", "template_error": "Feil ved gjengivelse av mal. Se gjennom loggen for mer informasjon.", "timeout": "Tidsavbrudd under innlasting av URL", "unable_still_load": "Kan ikke laste inn gyldig bilde fra URL-adresse for stillbilde (f.eks. ugyldig verts-, URL- eller godkjenningsfeil). Se gjennom loggen hvis du vil ha mer informasjon.", @@ -82,12 +64,6 @@ "description": "![Camera Still Image Preview]( {preview_url} )", "title": "Forh\u00e5ndsvisning" }, - "content_type": { - "data": { - "content_type": "Innholdstype" - }, - "description": "Angi innholdstypen for str\u00f8mmen." - }, "init": { "data": { "authentication": "Godkjenning", diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index e4ee551b524..f6a8704b9bc 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", - "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", - "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", "stream_no_route_to_host": "Nie mo\u017cna znale\u017a\u0107 hosta podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", - "stream_no_video": "Strumie\u0144 nie zawiera wideo", "stream_not_permitted": "Operacja nie jest dozwolona podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", - "stream_unauthorised": "Autoryzacja nie powiod\u0142a si\u0119 podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "template_error": "B\u0142\u0105d renderowania szablonu. Przejrzyj log, aby uzyska\u0107 wi\u0119cej informacji.", "timeout": "Przekroczono limit czasu podczas \u0142adowania adresu URL", "unable_still_load": "Nie mo\u017cna za\u0142adowa\u0107 prawid\u0142owego obrazu z adresu URL nieruchomego obrazu (np. nieprawid\u0142owy host, adres URL lub b\u0142\u0105d uwierzytelniania). Przejrzyj logi, aby uzyska\u0107 wi\u0119cej informacji.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { - "confirm": { - "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" - }, - "content_type": { - "data": { - "content_type": "Typ zawarto\u015bci" - }, - "description": "Okre\u015bl typ zawarto\u015bci strumienia." - }, "user": { "data": { "authentication": "Uwierzytelnianie", @@ -62,13 +48,9 @@ "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", - "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", - "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", "stream_no_route_to_host": "Nie mo\u017cna znale\u017a\u0107 hosta podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", - "stream_no_video": "Strumie\u0144 nie zawiera wideo", "stream_not_permitted": "Operacja nie jest dozwolona podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", - "stream_unauthorised": "Autoryzacja nie powiod\u0142a si\u0119 podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "template_error": "B\u0142\u0105d renderowania szablonu. Przejrzyj log, aby uzyska\u0107 wi\u0119cej informacji.", "timeout": "Przekroczono limit czasu podczas \u0142adowania adresu URL", "unable_still_load": "Nie mo\u017cna za\u0142adowa\u0107 prawid\u0142owego obrazu z adresu URL nieruchomego obrazu (np. nieprawid\u0142owy host, adres URL lub b\u0142\u0105d uwierzytelniania). Przejrzyj logi, aby uzyska\u0107 wi\u0119cej informacji.", @@ -82,12 +64,6 @@ "description": "![Podgl\u0105d nieruchomego obrazu z kamery]({preview_url})", "title": "Podgl\u0105d" }, - "content_type": { - "data": { - "content_type": "Typ zawarto\u015bci" - }, - "description": "Okre\u015bl typ zawarto\u015bci strumienia." - }, "init": { "data": { "authentication": "Uwierzytelnianie", diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index de9be64fd2a..e5fb0fb7db7 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", - "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", - "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", "stream_no_route_to_host": "N\u00e3o foi poss\u00edvel encontrar o host ao tentar se conectar a stream", - "stream_no_video": "A stream n\u00e3o tem v\u00eddeo", "stream_not_permitted": "Opera\u00e7\u00e3o n\u00e3o permitida ao tentar se conectar a stream. Protocolo RTSP errado?", - "stream_unauthorised": "Falha na autoriza\u00e7\u00e3o ao tentar se conectar a stream", "template_error": "Erro ao renderizar o modelo. Revise o registro para obter mais informa\u00e7\u00f5es.", "timeout": "Tempo limite ao carregar a URL", "unable_still_load": "N\u00e3o foi poss\u00edvel carregar uma imagem v\u00e1lida do URL da imagem est\u00e1tica (por exemplo, host inv\u00e1lido, URL ou falha de autentica\u00e7\u00e3o). Revise o log para obter mais informa\u00e7\u00f5es.", "unknown": "Erro inesperado" }, "step": { - "confirm": { - "description": "Deseja iniciar a configura\u00e7\u00e3o?" - }, - "content_type": { - "data": { - "content_type": "Tipo de conte\u00fado" - }, - "description": "Especifique o tipo de conte\u00fado para o stream." - }, "user": { "data": { "authentication": "Autentica\u00e7\u00e3o", @@ -62,13 +48,9 @@ "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", - "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", - "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", "stream_no_route_to_host": "N\u00e3o foi poss\u00edvel encontrar o host ao tentar se conectar a stream", - "stream_no_video": "A stream n\u00e3o tem v\u00eddeo", "stream_not_permitted": "Opera\u00e7\u00e3o n\u00e3o permitida ao tentar se conectar a stream. Protocolo RTSP errado?", - "stream_unauthorised": "Falha na autoriza\u00e7\u00e3o ao tentar se conectar a stream", "template_error": "Erro ao renderizar o modelo. Revise o registro para obter mais informa\u00e7\u00f5es.", "timeout": "Tempo limite ao carregar a URL", "unable_still_load": "N\u00e3o foi poss\u00edvel carregar uma imagem v\u00e1lida do URL da imagem est\u00e1tica (por exemplo, host inv\u00e1lido, URL ou falha de autentica\u00e7\u00e3o). Revise o log para obter mais informa\u00e7\u00f5es.", @@ -77,17 +59,11 @@ "step": { "confirm_still": { "data": { - "confirmed_ok": "Esta imagem parece boa." + "confirmed_ok": "Essa imagem parece boa." }, - "description": "![Camera Still Image Preview]({preview_url})", + "description": "![Visualiza\u00e7\u00e3o da imagem est\u00e1tica da c\u00e2mera]({preview_url})", "title": "Visualizar" }, - "content_type": { - "data": { - "content_type": "Tipo de conte\u00fado" - }, - "description": "Especifique o tipo de conte\u00fado para o stream." - }, "init": { "data": { "authentication": "Autentica\u00e7\u00e3o", diff --git a/homeassistant/components/generic/translations/pt.json b/homeassistant/components/generic/translations/pt.json index 06abd0a7dfa..d2f1d6aa0f9 100644 --- a/homeassistant/components/generic/translations/pt.json +++ b/homeassistant/components/generic/translations/pt.json @@ -2,11 +2,6 @@ "config": { "error": { "unknown": "Erro inesperado" - }, - "step": { - "confirm": { - "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" - } } }, "options": { diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index ad7126d85b6..c0e6ec31bfb 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "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.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", - "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", - "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_no_route_to_host": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0445\u043e\u0441\u0442 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", - "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", - "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { - "confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" - }, - "content_type": { - "data": { - "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e" - }, - "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0442\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0434\u043b\u044f \u043f\u043e\u0442\u043e\u043a\u0430." - }, "user": { "data": { "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", @@ -62,24 +48,21 @@ "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", - "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", - "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_no_route_to_host": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0445\u043e\u0441\u0442 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", - "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", - "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { - "content_type": { + "confirm_still": { "data": { - "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e" + "confirmed_ok": "\u042d\u0442\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0445\u043e\u0440\u043e\u0448\u043e." }, - "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0442\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0434\u043b\u044f \u043f\u043e\u0442\u043e\u043a\u0430." + "description": "![\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441 \u043a\u0430\u043c\u0435\u0440\u044b]({preview_url})", + "title": "\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440" }, "init": { "data": { diff --git a/homeassistant/components/generic/translations/sk.json b/homeassistant/components/generic/translations/sk.json new file mode 100644 index 00000000000..92f13ab5e49 --- /dev/null +++ b/homeassistant/components/generic/translations/sk.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "already_exists": "Kamera s t\u00fdmito nastaveniami URL u\u017e existuje.", + "invalid_still_image": "Adresa URL nevr\u00e1tila platn\u00fd statick\u00fd obr\u00e1zok", + "malformed_url": "Chybne vytvoren\u00e1 adresa URL", + "relative_url": "Relat\u00edvne adresy URL nie s\u00fa povolen\u00e9", + "stream_no_route_to_host": "Pri pokuse o pripojenie k streamu sa nepodarilo n\u00e1js\u0165 hostite\u013ea", + "timeout": "\u010casov\u00fd limit pri na\u010d\u00edtan\u00ed adresy URL", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "authentication": "Overenie", + "framerate": "Sn\u00edmkov\u00e1 frekvencia (Hz)", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + }, + "description": "Zadajte nastavenia na pripojenie ku kamere." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Tento obr\u00e1zok vyzer\u00e1 dobre." + }, + "description": "![Uk\u00e1\u017eka statick\u00e9ho obr\u00e1zka z fotoapar\u00e1tu]({preview_url})" + } + } + }, + "options": { + "error": { + "already_exists": "Kamera s t\u00fdmito nastaveniami URL u\u017e existuje.", + "invalid_still_image": "Adresa URL nevr\u00e1tila platn\u00fd statick\u00fd obr\u00e1zok", + "malformed_url": "Chybne vytvoren\u00e1 adresa URL", + "relative_url": "Relat\u00edvne adresy URL nie s\u00fa povolen\u00e9", + "stream_no_route_to_host": "Pri pokuse o pripojenie k streamu sa nepodarilo n\u00e1js\u0165 hostite\u013ea", + "timeout": "\u010casov\u00fd limit pri na\u010d\u00edtan\u00ed adresy URL", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "confirm_still": { + "data": { + "confirmed_ok": "Tento obr\u00e1zok vyzer\u00e1 dobre." + }, + "description": "![Uk\u00e1\u017eka statick\u00e9ho obr\u00e1zka z fotoapar\u00e1tu]({preview_url})" + }, + "init": { + "data": { + "authentication": "Overenie", + "framerate": "Sn\u00edmkov\u00e1 frekvencia (Hz)", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 4db8e007a1d..a794a478e76 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "Inga enheter hittades i n\u00e4tverket", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Ogiltig URL", "no_still_image_or_stream_url": "Du m\u00e5ste ange \u00e5tminstone en stillbilds- eller stream-URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tna", - "stream_file_not_found": "Filen hittades inte n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6m (\u00e4r ffmpeg installerat?)", - "stream_http_not_found": "HTTP 404 Ej funnen n\u00e4r du f\u00f6rs\u00f6ker ansluta till str\u00f6mmen", "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", "stream_no_route_to_host": "Kunde inte hitta v\u00e4rddatorn n\u00e4r jag f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", - "stream_no_video": "Str\u00f6mmen har ingen video", "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", - "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL", "unable_still_load": "Det g\u00e5r inte att ladda giltig bild fr\u00e5n stillbilds-URL (t.ex. ogiltig v\u00e4rd, URL eller autentiseringsfel). Granska loggen f\u00f6r mer information.", "unknown": "Ov\u00e4ntat fel" }, "step": { - "confirm": { - "description": "Vill du starta konfigurationen?" - }, - "content_type": { - "data": { - "content_type": "Inneh\u00e5llstyp" - }, - "description": "Ange inneh\u00e5llstypen f\u00f6r str\u00f6mmen." - }, "user": { "data": { "authentication": "Autentiseringen", @@ -62,25 +48,15 @@ "malformed_url": "Ogiltig URL", "no_still_image_or_stream_url": "Du m\u00e5ste ange \u00e5tminstone en stillbilds- eller stream-URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tet", - "stream_file_not_found": "Filen hittades inte n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6m (\u00e4r ffmpeg installerat?)", - "stream_http_not_found": "HTTP 404 Ej funnen n\u00e4r du f\u00f6rs\u00f6ker ansluta till str\u00f6mmen", "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", "stream_no_route_to_host": "Kunde inte hitta v\u00e4rddatorn n\u00e4r jag f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", - "stream_no_video": "Str\u00f6mmen har ingen video", "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", - "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL", "unable_still_load": "Det g\u00e5r inte att ladda giltig bild fr\u00e5n stillbilds-URL (t.ex. ogiltig v\u00e4rd, URL eller autentiseringsfel). Granska loggen f\u00f6r mer information.", "unknown": "Ov\u00e4ntat fel" }, "step": { - "content_type": { - "data": { - "content_type": "Inneh\u00e5llstyp" - }, - "description": "Ange tyen av inneh\u00e5ll f\u00f6r str\u00f6mmen" - }, "init": { "data": { "authentication": "Autentiseringen", diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index c3561d18e2a..c6cc30bb377 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "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." }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", "relative_url": "G\u00f6receli URL'lere izin verilmez", - "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", - "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", "stream_no_route_to_host": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken ana bilgisayar bulunamad\u0131", - "stream_no_video": "Ak\u0131\u015fta video yok", "stream_not_permitted": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken i\u015fleme izin verilmiyor. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", - "stream_unauthorised": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken yetkilendirme ba\u015far\u0131s\u0131z oldu", "template_error": "\u015eablon olu\u015fturma hatas\u0131. Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", "timeout": "URL y\u00fcklenirken zaman a\u015f\u0131m\u0131", "unable_still_load": "Hareketsiz resim URL'sinden ge\u00e7erli resim y\u00fcklenemiyor (\u00f6r. ge\u00e7ersiz ana bilgisayar, URL veya kimlik do\u011frulama hatas\u0131). Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", "unknown": "Beklenmeyen hata" }, "step": { - "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" - }, - "content_type": { - "data": { - "content_type": "\u0130\u00e7erik T\u00fcr\u00fc" - }, - "description": "Ak\u0131\u015f i\u00e7in i\u00e7erik t\u00fcr\u00fcn\u00fc belirtin." - }, "user": { "data": { "authentication": "Kimlik Do\u011frulama", @@ -62,13 +48,9 @@ "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", "relative_url": "G\u00f6receli URL'lere izin verilmez", - "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", - "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", "stream_no_route_to_host": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken ana bilgisayar bulunamad\u0131", - "stream_no_video": "Ak\u0131\u015fta video yok", "stream_not_permitted": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken i\u015fleme izin verilmiyor. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", - "stream_unauthorised": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken yetkilendirme ba\u015far\u0131s\u0131z oldu", "template_error": "\u015eablon olu\u015fturma hatas\u0131. Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", "timeout": "URL y\u00fcklenirken zaman a\u015f\u0131m\u0131", "unable_still_load": "Hareketsiz resim URL'sinden ge\u00e7erli resim y\u00fcklenemiyor (\u00f6r. ge\u00e7ersiz ana bilgisayar, URL veya kimlik do\u011frulama hatas\u0131). Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", @@ -82,12 +64,6 @@ "description": "![Kamera Dura\u011fan G\u00f6r\u00fcnt\u00fc \u00d6nizlemesi]( {preview_url} )", "title": "\u00d6nizleme" }, - "content_type": { - "data": { - "content_type": "\u0130\u00e7erik T\u00fcr\u00fc" - }, - "description": "Ak\u0131\u015f i\u00e7in i\u00e7erik t\u00fcr\u00fcn\u00fc belirtin." - }, "init": { "data": { "authentication": "Kimlik Do\u011frulama", diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index 09203276fda..34a8d5c6f78 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { @@ -10,28 +9,15 @@ "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", - "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", - "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", "stream_no_route_to_host": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u627e\u4e0d\u5230\u4e3b\u6a5f", - "stream_no_video": "\u4e32\u6d41\u6c92\u6709\u5f71\u50cf", "stream_not_permitted": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u4e0d\u5141\u8a31\u64cd\u4f5c\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", - "stream_unauthorised": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u8a8d\u8b49\u5931\u6557", "template_error": "\u6a21\u7248\u6e32\u67d3\u932f\u8aa4\u3001\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u6599\u3002", "timeout": "\u8f09\u5165 URL \u903e\u6642\u6642\u9593", "unable_still_load": "\u7121\u6cd5\u7531\u8a2d\u5b9a\u975c\u614b\u5f71\u50cf URL \u8f09\u5165\u6709\u6548\u5f71\u50cf\uff08\u4f8b\u5982\uff1a\u7121\u6548\u4e3b\u6a5f\u3001URL \u6216\u8a8d\u8b49\u5931\u6557\uff09\u3002\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8a0a\u606f\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { - "confirm": { - "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" - }, - "content_type": { - "data": { - "content_type": "\u5167\u5bb9\u985e\u578b" - }, - "description": "\u6307\u5b9a\u4e32\u6d41\u5167\u5bb9\u985e\u5225" - }, "user": { "data": { "authentication": "\u9a57\u8b49", @@ -48,7 +34,7 @@ }, "user_confirm_still": { "data": { - "confirmed_ok": "\u5f71\u50cf\u633a\u6e05\u6670\u3002" + "confirmed_ok": "\u5f71\u50cf\u770b\u8d77\u4f86\u5f88\u6e05\u6670" }, "description": "![\u651d\u5f71\u6a5f\u975c\u614b\u9810\u89bd]({preview_url})", "title": "\u9810\u89bd" @@ -62,13 +48,9 @@ "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", - "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", - "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", "stream_no_route_to_host": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u627e\u4e0d\u5230\u4e3b\u6a5f", - "stream_no_video": "\u4e32\u6d41\u6c92\u6709\u5f71\u50cf", "stream_not_permitted": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u4e0d\u5141\u8a31\u64cd\u4f5c\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", - "stream_unauthorised": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u8a8d\u8b49\u5931\u6557", "template_error": "\u6a21\u7248\u6e32\u67d3\u932f\u8aa4\u3001\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u6599\u3002", "timeout": "\u8f09\u5165 URL \u903e\u6642\u6642\u9593", "unable_still_load": "\u7121\u6cd5\u7531\u8a2d\u5b9a\u975c\u614b\u5f71\u50cf URL \u8f09\u5165\u6709\u6548\u5f71\u50cf\uff08\u4f8b\u5982\uff1a\u7121\u6548\u4e3b\u6a5f\u3001URL \u6216\u8a8d\u8b49\u5931\u6557\uff09\u3002\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8a0a\u606f\u3002", @@ -77,17 +59,11 @@ "step": { "confirm_still": { "data": { - "confirmed_ok": "\u5f71\u50cf\u633a\u6e05\u6670\u3002" + "confirmed_ok": "\u5f71\u50cf\u770b\u8d77\u4f86\u5f88\u6e05\u6670" }, "description": "![\u651d\u5f71\u6a5f\u975c\u614b\u9810\u89bd]({preview_url})", "title": "\u9810\u89bd" }, - "content_type": { - "data": { - "content_type": "\u5167\u5bb9\u985e\u578b" - }, - "description": "\u6307\u5b9a\u4e32\u6d41\u5167\u5bb9\u985e\u5225" - }, "init": { "data": { "authentication": "\u9a57\u8b49", @@ -97,7 +73,7 @@ "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a", "still_image_url": "\u975c\u614b\u5f71\u50cf URL\uff08\u4f8b\u5982 http://...\uff09", "stream_source": "\u4e32\u6d41\u4f86\u6e90 URL\uff08\u4f8b\u5982 rtsp://...\uff09", - "use_wallclock_as_timestamps": "\u4f7f\u7528\u639b\u9418\u4f5c\u70ba\u6642\u9593\u6233", + "use_wallclock_as_timestamps": "\u4f7f\u7528\u6642\u9418\u4f5c\u70ba\u6642\u9593\u6233", "username": "\u4f7f\u7528\u8005\u540d\u7a31", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" }, diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 720c76e766d..e6caf2ca097 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -147,7 +147,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): self._min_humidity = min_humidity self._max_humidity = max_humidity self._target_humidity = target_humidity - self._attr_supported_features = 0 if away_humidity: self._attr_supported_features |= HumidifierEntityFeature.MODES self._away_humidity = away_humidity diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 20acb533a2c..06237b6e8d5 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -81,7 +81,7 @@ class GeniusBattery(GeniusDevice, SensorEntity): return icon @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.BATTERY diff --git a/homeassistant/components/geocaching/translations/bg.json b/homeassistant/components/geocaching/translations/bg.json index d3ca579dff7..3004464ff5b 100644 --- a/homeassistant/components/geocaching/translations/bg.json +++ b/homeassistant/components/geocaching/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u043e" diff --git a/homeassistant/components/geocaching/translations/cs.json b/homeassistant/components/geocaching/translations/cs.json index 5b7d9c2db8e..67665aec47e 100644 --- a/homeassistant/components/geocaching/translations/cs.json +++ b/homeassistant/components/geocaching/translations/cs.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "oauth_error": "P\u0159ijata neplatn\u00e1 data tokenu.", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "step": { diff --git a/homeassistant/components/geocaching/translations/sk.json b/homeassistant/components/geocaching/translations/sk.json new file mode 100644 index 00000000000..50a9875493e --- /dev/null +++ b/homeassistant/components/geocaching/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "description": "Integr\u00e1cia slu\u017eby Geocaching potrebuje op\u00e4tovn\u00e9 overenie v\u00e1\u0161ho konta", + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 85197239ccd..cc47883d05a 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,6 +1,5 @@ """Support for the Geofency device tracker platform.""" -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/geofency/translations/sk.json b/homeassistant/components/geofency/translations/sk.json new file mode 100644 index 00000000000..933f73976d2 --- /dev/null +++ b/homeassistant/components/geofency/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index ba8eecc4ae9..dd62682e304 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@exxamalte"], "quality_scale": "platinum", "iot_class": "cloud_polling", - "loggers": ["aio_geojson_geonetnz_quakes"] + "loggers": ["aio_geojson_geonetnz_quakes"], + "integration_type": "service" } diff --git a/homeassistant/components/geonetnz_quakes/translations/sk.json b/homeassistant/components/geonetnz_quakes/translations/sk.json new file mode 100644 index 00000000000..87f55b18c6f --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "R\u00e1dius" + }, + "title": "Vypl\u0148te podrobnosti filtra." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index a365237561a..7c765ecb939 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aio_geojson_geonetnz_volcano==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["aio_geojson_geonetnz_volcano"] + "loggers": ["aio_geojson_geonetnz_volcano"], + "integration_type": "service" } diff --git a/homeassistant/components/geonetnz_volcano/translations/sk.json b/homeassistant/components/geonetnz_volcano/translations/sk.json new file mode 100644 index 00000000000..0e1348ce290 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "radius": "R\u00e1dius" + }, + "title": "Vypl\u0148te podrobnosti filtra." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/sk.json b/homeassistant/components/gios/translations/sk.json index af15f92c2f2..23559b853da 100644 --- a/homeassistant/components/gios/translations/sk.json +++ b/homeassistant/components/gios/translations/sk.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_sensors_data": "Neplatn\u00e9 \u00fadaje sn\u00edma\u010dov pre t\u00fato meraciu stanicu.", + "wrong_station_id": "ID meracej stanice nie je spr\u00e1vne." + }, "step": { "user": { "data": { - "name": "N\u00e1zov" + "name": "N\u00e1zov", + "station_id": "ID meracej stanice" } } } diff --git a/homeassistant/components/github/translations/de.json b/homeassistant/components/github/translations/de.json index 0d195210744..8446b35f201 100644 --- a/homeassistant/components/github/translations/de.json +++ b/homeassistant/components/github/translations/de.json @@ -10,9 +10,9 @@ "step": { "repositories": { "data": { - "repositories": "W\u00e4hle die zu verfolgenden Repositories aus." + "repositories": "W\u00e4hle die zu verfolgenden Repositorien aus." }, - "title": "Repositories konfigurieren" + "title": "Repositorien konfigurieren" } } } diff --git a/homeassistant/components/github/translations/it.json b/homeassistant/components/github/translations/it.json index a1cb5006305..08eee73fc1c 100644 --- a/homeassistant/components/github/translations/it.json +++ b/homeassistant/components/github/translations/it.json @@ -10,9 +10,9 @@ "step": { "repositories": { "data": { - "repositories": "Seleziona i repository da tracciare." + "repositories": "Seleziona gli archivi digitali da monitorare." }, - "title": "Configura repository" + "title": "Configura gli archivi digitali" } } } diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 0747db89cd2..bda1baf797a 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -1,27 +1,16 @@ """The Glances component.""" -from datetime import timedelta -import logging +from typing import Any -from glances_api import Glances, exceptions +from glances_api import Glances from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_VERIFY_SSL, - Platform, -) +from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.httpx_client import get_async_client -from .const import DATA_UPDATED, DEFAULT_SCAN_INTERVAL, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN +from .coordinator import GlancesDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -30,106 +19,28 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Glances from config entry.""" - client = GlancesData(hass, config_entry) - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client - if not await client.async_setup(): - return False + api = get_api(hass, dict(config_entry.data)) + coordinator = GlancesDataUpdateCoordinator(hass, config_entry, api) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] return unload_ok -class GlancesData: - """Get the latest data from Glances api.""" - - def __init__(self, hass, config_entry): - """Initialize the Glances data.""" - self.hass = hass - self.config_entry = config_entry - self.api = None - self.unsub_timer = None - self.available = False - - @property - def host(self): - """Return client host.""" - return self.config_entry.data[CONF_HOST] - - async def async_update(self): - """Get the latest data from the Glances REST API.""" - try: - await self.api.get_data("all") - self.available = True - except exceptions.GlancesApiError: - _LOGGER.error("Unable to fetch data from Glances") - self.available = False - _LOGGER.debug("Glances data updated") - async_dispatcher_send(self.hass, DATA_UPDATED) - - async def async_setup(self): - """Set up the Glances client.""" - try: - self.api = get_api(self.hass, self.config_entry.data) - await self.api.get_data("all") - self.available = True - _LOGGER.debug("Successfully connected to Glances") - - except exceptions.GlancesApiConnectionError as err: - _LOGGER.debug("Can not connect to Glances") - raise ConfigEntryNotReady from err - - self.add_options() - self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - self.config_entry.async_on_unload( - self.config_entry.add_update_listener(self.async_options_updated) - ) - - await self.hass.config_entries.async_forward_entry_setups( - self.config_entry, PLATFORMS - ) - - return True - - def add_options(self): - """Add options for Glances integration.""" - if not self.config_entry.options: - options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} - self.hass.config_entries.async_update_entry( - self.config_entry, options=options - ) - - def set_scan_interval(self, scan_interval): - """Update scan interval.""" - - async def refresh(event_time): - """Get the latest data from Glances api.""" - await self.async_update() - - if self.unsub_timer is not None: - self.unsub_timer() - self.unsub_timer = async_track_time_interval( - self.hass, refresh, timedelta(seconds=scan_interval) - ) - - @staticmethod - async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Triggered by config entry options updates.""" - hass.data[DOMAIN][entry.entry_id].set_scan_interval( - entry.options[CONF_SCAN_INTERVAL] - ) - - -def get_api(hass, entry): +def get_api(hass: HomeAssistant, entry_data: dict[str, Any]) -> Glances: """Return the api from glances_api.""" - params = entry.copy() - params.pop(CONF_NAME, None) - verify_ssl = params.pop(CONF_VERIFY_SSL, True) - httpx_client = get_async_client(hass, verify_ssl=verify_ssl) - return Glances(httpx_client=httpx_client, **params) + entry_data.pop(CONF_NAME, None) + httpx_client = get_async_client(hass, verify_ssl=entry_data[CONF_VERIFY_SSL]) + return Glances(httpx_client=httpx_client, **entry_data) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index a56fa795491..cf55118a913 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -3,20 +3,19 @@ from __future__ import annotations from typing import Any -import glances_api +from glances_api.exceptions import GlancesApiError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from . import get_api @@ -24,7 +23,6 @@ from .const import ( CONF_VERSION, DEFAULT_HOST, DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, DEFAULT_VERSION, DOMAIN, SUPPORTED_VERSIONS, @@ -43,12 +41,12 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: """Validate the user input allows us to connect.""" + api = get_api(hass, data) try: - api = get_api(hass, data) await api.get_data("all") - except glances_api.exceptions.GlancesApiConnectionError as err: + except GlancesApiError as err: raise CannotConnect from err @@ -57,14 +55,6 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> GlancesOptionsFlowHandler: - """Get the options flow for this handler.""" - return GlancesOptionsFlowHandler(config_entry) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -85,31 +75,5 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class GlancesOptionsFlowHandler(config_entries.OptionsFlow): - """Handle Glances client options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize Glances options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the Glances options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - options = { - vol.Optional( - CONF_SCAN_INTERVAL, - default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ), - ): int - } - - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) - - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index efcc30c057b..b704ab326f4 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -10,7 +10,6 @@ DEFAULT_PORT = 61208 DEFAULT_VERSION = 3 DEFAULT_SCAN_INTERVAL = 60 -DATA_UPDATED = "glances_data_updated" SUPPORTED_VERSIONS = [2, 3] CPU_ICON = f"mdi:cpu-{64 if sys.maxsize > 2**32 else 32}-bit" diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py new file mode 100644 index 00000000000..8ffd2a2da6e --- /dev/null +++ b/homeassistant/components/glances/coordinator.py @@ -0,0 +1,42 @@ +"""Coordinator for Glances integration.""" +from datetime import timedelta +import logging +from typing import Any + +from glances_api import Glances, exceptions + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Get the latest data from Glances api.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api: Glances) -> None: + """Initialize the Glances data.""" + self.hass = hass + self.config_entry = entry + self.host: str = entry.data[CONF_HOST] + self.api = api + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN} - {self.host}", + update_interval=timedelta(seconds=60), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Get the latest data from the Glances REST API.""" + try: + await self.api.get_data("all") + except exceptions.GlancesApiError as err: + raise UpdateFailed from err + return self.api.data diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 13f4284acd3..a479cb260de 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -8,10 +8,10 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, SensorStateClass, + StateType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_HOST, CONF_NAME, DATA_GIBIBYTES, DATA_MEBIBYTES, @@ -21,22 +21,29 @@ from homeassistant.const import ( TEMP_CELSIUS, Platform, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import GlancesData -from .const import CPU_ICON, DATA_UPDATED, DOMAIN +from . import GlancesDataUpdateCoordinator +from .const import CPU_ICON, DOMAIN @dataclass -class GlancesSensorEntityDescription(SensorEntityDescription): - """Describe Glances sensor entity.""" +class GlancesSensorEntityDescriptionMixin: + """Mixin for required keys.""" - type: str | None = None - name_suffix: str | None = None + type: str + name_suffix: str + + +@dataclass +class GlancesSensorEntityDescription( + SensorEntityDescription, GlancesSensorEntityDescriptionMixin +): + """Describe Glances sensor entity.""" SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( @@ -234,9 +241,9 @@ async def async_setup_entry( ) -> None: """Set up the Glances sensors.""" - client: GlancesData = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GlancesDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data.get(CONF_NAME) - dev = [] + entities = [] @callback def _migrate_old_unique_ids( @@ -256,15 +263,15 @@ async def async_setup_entry( for description in SENSOR_TYPES: if description.type == "fs": # fs will provide a list of disks attached - for disk in client.api.data[description.type]: + for disk in coordinator.data[description.type]: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {disk['mnt_point']} {description.name_suffix}", + f"{coordinator.host}-{name} {disk['mnt_point']} {description.name_suffix}", f"{disk['mnt_point']}-{description.key}", ) - dev.append( + entities.append( GlancesSensor( - client, + coordinator, name, disk["mnt_point"], description, @@ -272,101 +279,80 @@ async def async_setup_entry( ) elif description.type == "sensors": # sensors will provide temp for different devices - for sensor in client.api.data[description.type]: + for sensor in coordinator.data[description.type]: if sensor["type"] == description.key: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {sensor['label']} {description.name_suffix}", + f"{coordinator.host}-{name} {sensor['label']} {description.name_suffix}", f"{sensor['label']}-{description.key}", ) - dev.append( + entities.append( GlancesSensor( - client, + coordinator, name, sensor["label"], description, ) ) elif description.type == "raid": - for raid_device in client.api.data[description.type]: + for raid_device in coordinator.data[description.type]: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {raid_device} {description.name_suffix}", + f"{coordinator.host}-{name} {raid_device} {description.name_suffix}", f"{raid_device}-{description.key}", ) - dev.append(GlancesSensor(client, name, raid_device, description)) - elif client.api.data[description.type]: + entities.append( + GlancesSensor(coordinator, name, raid_device, description) + ) + elif coordinator.data[description.type]: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {description.name_suffix}", + f"{coordinator.host}-{name} {description.name_suffix}", f"-{description.key}", ) - dev.append( + entities.append( GlancesSensor( - client, + coordinator, name, "", description, ) ) - async_add_entities(dev, True) + async_add_entities(entities) -class GlancesSensor(SensorEntity): +class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntity): """Implementation of a Glances sensor.""" entity_description: GlancesSensorEntityDescription _attr_has_entity_name = True - _attr_should_poll = False def __init__( self, - glances_data: GlancesData, + coordinator: GlancesDataUpdateCoordinator, name: str | None, sensor_name_prefix: str, description: GlancesSensorEntityDescription, ) -> None: """Initialize the sensor.""" - self.glances_data = glances_data + super().__init__(coordinator) self._sensor_name_prefix = sensor_name_prefix - self.unsub_update: CALLBACK_TYPE | None = None - self.entity_description = description - self._attr_name = f"{sensor_name_prefix} {description.name_suffix}" + self._attr_name = f"{sensor_name_prefix} {description.name_suffix}".strip() self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, glances_data.config_entry.entry_id)}, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, manufacturer="Glances", - name=name or glances_data.config_entry.data[CONF_HOST], + name=name or coordinator.host, ) - self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @property - def available(self) -> bool: - """Could the device be accessed during the last update call.""" - return self.glances_data.available - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - self.unsub_update = async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update - ) - - @callback - def _schedule_immediate_update(self) -> None: - self.async_schedule_update_ha_state(True) - - async def will_remove_from_hass(self) -> None: - """Unsubscribe from update dispatcher.""" - if self.unsub_update: - self.unsub_update() - self.unsub_update = None - - async def async_update(self) -> None: # noqa: C901 - """Get the latest data from REST API.""" - if (value := self.glances_data.api.data) is None: - return - + def native_value(self) -> StateType: # noqa: C901 + """Return the state of the resources.""" + if (value := self.coordinator.data) is None: + return None + state: StateType = None if self.entity_description.type == "fs": for var in value["fs"]: if var["mnt_point"] == self._sensor_name_prefix: @@ -374,100 +360,102 @@ class GlancesSensor(SensorEntity): break if self.entity_description.key == "disk_free": try: - self._attr_native_value = round(disk["free"] / 1024**3, 1) + state = round(disk["free"] / 1024**3, 1) except KeyError: - self._attr_native_value = round( + state = round( (disk["size"] - disk["used"]) / 1024**3, 1, ) elif self.entity_description.key == "disk_use": - self._attr_native_value = round(disk["used"] / 1024**3, 1) + state = round(disk["used"] / 1024**3, 1) elif self.entity_description.key == "disk_use_percent": - self._attr_native_value = disk["percent"] + state = disk["percent"] elif self.entity_description.key == "battery": for sensor in value["sensors"]: if ( sensor["type"] == "battery" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "fan_speed": for sensor in value["sensors"]: if ( sensor["type"] == "fan_speed" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "temperature_core": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_core" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "temperature_hdd": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_hdd" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "memory_use_percent": - self._attr_native_value = value["mem"]["percent"] + state = value["mem"]["percent"] elif self.entity_description.key == "memory_use": - self._attr_native_value = round(value["mem"]["used"] / 1024**2, 1) + state = round(value["mem"]["used"] / 1024**2, 1) elif self.entity_description.key == "memory_free": - self._attr_native_value = round(value["mem"]["free"] / 1024**2, 1) + state = round(value["mem"]["free"] / 1024**2, 1) elif self.entity_description.key == "swap_use_percent": - self._attr_native_value = value["memswap"]["percent"] + state = value["memswap"]["percent"] elif self.entity_description.key == "swap_use": - self._attr_native_value = round(value["memswap"]["used"] / 1024**3, 1) + state = round(value["memswap"]["used"] / 1024**3, 1) elif self.entity_description.key == "swap_free": - self._attr_native_value = round(value["memswap"]["free"] / 1024**3, 1) + state = round(value["memswap"]["free"] / 1024**3, 1) elif self.entity_description.key == "processor_load": # Windows systems don't provide load details try: - self._attr_native_value = value["load"]["min15"] + state = value["load"]["min15"] except KeyError: - self._attr_native_value = value["cpu"]["total"] + state = value["cpu"]["total"] elif self.entity_description.key == "process_running": - self._attr_native_value = value["processcount"]["running"] + state = value["processcount"]["running"] elif self.entity_description.key == "process_total": - self._attr_native_value = value["processcount"]["total"] + state = value["processcount"]["total"] elif self.entity_description.key == "process_thread": - self._attr_native_value = value["processcount"]["thread"] + state = value["processcount"]["thread"] elif self.entity_description.key == "process_sleeping": - self._attr_native_value = value["processcount"]["sleeping"] + state = value["processcount"]["sleeping"] elif self.entity_description.key == "cpu_use_percent": - self._attr_native_value = value["quicklook"]["cpu"] + state = value["quicklook"]["cpu"] elif self.entity_description.key == "docker_active": count = 0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: count += 1 - self._attr_native_value = count + state = count except KeyError: - self._attr_native_value = count + state = count elif self.entity_description.key == "docker_cpu_use": cpu_use = 0.0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: cpu_use += container["cpu"]["total"] - self._attr_native_value = round(cpu_use, 1) + state = round(cpu_use, 1) except KeyError: - self._attr_native_value = STATE_UNAVAILABLE + state = STATE_UNAVAILABLE elif self.entity_description.key == "docker_memory_use": mem_use = 0.0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: mem_use += container["memory"]["usage"] - self._attr_native_value = round(mem_use / 1024**2, 1) + state = round(mem_use / 1024**2, 1) except KeyError: - self._attr_native_value = STATE_UNAVAILABLE + state = STATE_UNAVAILABLE elif self.entity_description.type == "raid": for raid_device, raid in value["raid"].items(): if raid_device == self._sensor_name_prefix: - self._attr_native_value = raid[self.entity_description.key] + state = raid[self.entity_description.key] + + return state diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index 11c9792f364..b46716b43c0 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -14,21 +14,10 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "wrong_version": "Version not supported (2 or 3 only)" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "options": { - "step": { - "init": { - "description": "Configure options for Glances", - "data": { - "scan_interval": "Update frequency" - } - } - } } } diff --git a/homeassistant/components/glances/translations/bg.json b/homeassistant/components/glances/translations/bg.json index ef60201a57f..86c979aed41 100644 --- a/homeassistant/components/glances/translations/bg.json +++ b/homeassistant/components/glances/translations/bg.json @@ -4,32 +4,19 @@ "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d." }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", - "wrong_version": "\u0412\u0435\u0440\u0441\u0438\u044f\u0442\u0430 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 (\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0438 \u0432\u0435\u0440\u0441\u0438\u0438: 2 \u0438\u043b\u0438 3)" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430" }, "step": { "user": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", - "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 SSL/TLS, \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u043a\u044a\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430 Glances", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430", "version": "Glances API \u0432\u0435\u0440\u0441\u0438\u044f (2 \u0438\u043b\u0438 3)" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" - }, - "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 Glances" + } } } } diff --git a/homeassistant/components/glances/translations/ca.json b/homeassistant/components/glances/translations/ca.json index 1ef17e201a4..b3d372a39bc 100644 --- a/homeassistant/components/glances/translations/ca.json +++ b/homeassistant/components/glances/translations/ca.json @@ -4,32 +4,19 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "wrong_version": "Versi\u00f3 no compatible (2 o 3 necess\u00e0ria)" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { "user": { "data": { "host": "Amfitri\u00f3", - "name": "Nom", "password": "Contrasenya", "port": "Port", "ssl": "Utilitza un certificat SSL", "username": "Nom d'usuari", "verify_ssl": "Verifica el certificat SSL", "version": "Versi\u00f3 de l'API de Glances (2 o 3)" - }, - "title": "Configuraci\u00f3 de Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" - }, - "description": "Opcions de configuraci\u00f3 de Glances" + } } } } diff --git a/homeassistant/components/glances/translations/cs.json b/homeassistant/components/glances/translations/cs.json index 198731bdb3e..a1faf48c3bc 100644 --- a/homeassistant/components/glances/translations/cs.json +++ b/homeassistant/components/glances/translations/cs.json @@ -4,32 +4,19 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "wrong_version": "Verze nen\u00ed podporov\u00e1na (pouze 2 nebo 3)" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "user": { "data": { "host": "Hostitel", - "name": "Jm\u00e9no", "password": "Heslo", "port": "Port", "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t", "username": "U\u017eivatelsk\u00e9 jm\u00e9no", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL", "version": "Verze API pro Glances (2 nebo 3)" - }, - "title": "Nastavte Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frekvence aktualizac\u00ed" - }, - "description": "Nastavte mo\u017enosti pro Glances" + } } } } diff --git a/homeassistant/components/glances/translations/da.json b/homeassistant/components/glances/translations/da.json index 995ae9d3bba..b95ec888446 100644 --- a/homeassistant/components/glances/translations/da.json +++ b/homeassistant/components/glances/translations/da.json @@ -4,32 +4,19 @@ "already_configured": "V\u00e6rten er allerede konfigureret." }, "error": { - "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", - "wrong_version": "Version underst\u00f8ttes ikke (kun 2 eller 3)" + "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt" }, "step": { "user": { "data": { "host": "V\u00e6rt", - "name": "Navn", "password": "Adgangskode", "port": "Port", "ssl": "Brug SSL/TLS til at oprette forbindelse til Glances-systemet", "username": "Brugernavn", "verify_ssl": "Bekr\u00e6ft certificering af systemet", "version": "Glances API version (2 eller 3)" - }, - "title": "Ops\u00e6tning af Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Opdateringsfrekvens" - }, - "description": "Konfigurationsindstillinger for Glances" + } } } } diff --git a/homeassistant/components/glances/translations/de.json b/homeassistant/components/glances/translations/de.json index 8c91e4fb2e3..a642e25a80e 100644 --- a/homeassistant/components/glances/translations/de.json +++ b/homeassistant/components/glances/translations/de.json @@ -4,32 +4,19 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { "data": { "host": "Host", - "name": "Name", "password": "Passwort", "port": "Port", "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen", "version": "Glances API-Version (2 oder 3)" - }, - "title": "Glances einrichten" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Aktualisierungsfrequenz" - }, - "description": "Konfiguriere die Optionen f\u00fcr Glances" + } } } } diff --git a/homeassistant/components/glances/translations/el.json b/homeassistant/components/glances/translations/el.json index f0f927fcc71..c5ccbb28bb2 100644 --- a/homeassistant/components/glances/translations/el.json +++ b/homeassistant/components/glances/translations/el.json @@ -4,32 +4,19 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "wrong_version": "\u0397 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 (\u03bc\u03cc\u03bd\u03bf 2 \u03ae 3)" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "port": "\u0398\u03cd\u03c1\u03b1", "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 API Glances (2 \u03ae 3)" - }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u03a3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2" - }, - "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf Glances" + } } } } diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json index 87c53c3cf48..425fba95703 100644 --- a/homeassistant/components/glances/translations/en.json +++ b/homeassistant/components/glances/translations/en.json @@ -4,32 +4,19 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect", - "wrong_version": "Version not supported (2 or 3 only)" + "cannot_connect": "Failed to connect" }, "step": { "user": { "data": { "host": "Host", - "name": "Name", "password": "Password", "port": "Port", "ssl": "Uses an SSL certificate", "username": "Username", "verify_ssl": "Verify SSL certificate", "version": "Glances API Version (2 or 3)" - }, - "title": "Setup Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Update frequency" - }, - "description": "Configure options for Glances" + } } } } diff --git a/homeassistant/components/glances/translations/es-419.json b/homeassistant/components/glances/translations/es-419.json index 5e060b20d47..3cee11a4d01 100644 --- a/homeassistant/components/glances/translations/es-419.json +++ b/homeassistant/components/glances/translations/es-419.json @@ -4,32 +4,19 @@ "already_configured": "El host ya est\u00e1 configurado." }, "error": { - "cannot_connect": "No se puede conectar al host", - "wrong_version": "Versi\u00f3n no compatible (2 o 3 solamente)" + "cannot_connect": "No se puede conectar al host" }, "step": { "user": { "data": { "host": "Host", - "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Use SSL/TLS para conectarse al sistema Glances", "username": "Nombre de usuario", "verify_ssl": "Verificar la certificaci\u00f3n del sistema", "version": "Versi\u00f3n de API de Glances (2 o 3)" - }, - "title": "Configurar Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frecuencia de actualizaci\u00f3n" - }, - "description": "Configurar opciones para Glances" + } } } } diff --git a/homeassistant/components/glances/translations/es.json b/homeassistant/components/glances/translations/es.json index 22187e65793..e08c1162721 100644 --- a/homeassistant/components/glances/translations/es.json +++ b/homeassistant/components/glances/translations/es.json @@ -4,32 +4,19 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar", - "wrong_version": "Versi\u00f3n no soportada (s\u00f3lo 2 o 3)" + "cannot_connect": "No se pudo conectar" }, "step": { "user": { "data": { "host": "Host", - "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Utiliza un certificado SSL", "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL", "version": "Versi\u00f3n API de Glances (2 o 3)" - }, - "title": "Configurar Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frecuencia de actualizaci\u00f3n" - }, - "description": "Configurar opciones para Glances" + } } } } diff --git a/homeassistant/components/glances/translations/et.json b/homeassistant/components/glances/translations/et.json index 7f5e1bb63be..2135321945b 100644 --- a/homeassistant/components/glances/translations/et.json +++ b/homeassistant/components/glances/translations/et.json @@ -4,32 +4,19 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "cannot_connect": "\u00dchendamine nurjus", - "wrong_version": "Versiooni ei toetata (ainult 2 v\u00f5i 3)" + "cannot_connect": "\u00dchendamine nurjus" }, "step": { "user": { "data": { "host": "", - "name": "Nimi", "password": "Salas\u00f5na", "port": "Port", "ssl": "Kasutab SSL serti", "username": "Kasutajanimi", "verify_ssl": "Kontrolli SSL sertifikaati", "version": "Glances API versioon (2 v\u00f5i 3)" - }, - "title": "Seadista Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "V\u00e4rskendussagedus" - }, - "description": "Seadista Glances valikud" + } } } } diff --git a/homeassistant/components/glances/translations/fi.json b/homeassistant/components/glances/translations/fi.json index 70a013677c0..053107f5939 100644 --- a/homeassistant/components/glances/translations/fi.json +++ b/homeassistant/components/glances/translations/fi.json @@ -6,7 +6,6 @@ "step": { "user": { "data": { - "name": "Nimi", "password": "Salasana", "port": "portti" } diff --git a/homeassistant/components/glances/translations/fr.json b/homeassistant/components/glances/translations/fr.json index 6fafa8a3a51..05777784a4d 100644 --- a/homeassistant/components/glances/translations/fr.json +++ b/homeassistant/components/glances/translations/fr.json @@ -4,32 +4,19 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "\u00c9chec de connexion", - "wrong_version": "Version non prise en charge (2 ou 3 uniquement)" + "cannot_connect": "\u00c9chec de connexion" }, "step": { "user": { "data": { "host": "H\u00f4te", - "name": "Nom", "password": "Mot de passe", "port": "Port", "ssl": "Utilise un certificat SSL", "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL", "version": "Glances API Version (2 ou 3)" - }, - "title": "Installation de Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" - }, - "description": "Configurer les options pour Glances" + } } } } diff --git a/homeassistant/components/glances/translations/he.json b/homeassistant/components/glances/translations/he.json index f5ba6464a4e..e7c469e452a 100644 --- a/homeassistant/components/glances/translations/he.json +++ b/homeassistant/components/glances/translations/he.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", - "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "port": "\u05e4\u05ea\u05d7\u05d4", "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", diff --git a/homeassistant/components/glances/translations/hu.json b/homeassistant/components/glances/translations/hu.json index 71649b51d34..22dfa8a46dd 100644 --- a/homeassistant/components/glances/translations/hu.json +++ b/homeassistant/components/glances/translations/hu.json @@ -4,32 +4,19 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "wrong_version": "Nem t\u00e1mogatott verzi\u00f3 (2 vagy 3 csak)" + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { "data": { "host": "C\u00edm", - "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se", "version": "Glances API-verzi\u00f3 (2 vagy 3)" - }, - "title": "Glances Be\u00e1ll\u00edt\u00e1sa" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" - }, - "description": "A Glances be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa" + } } } } diff --git a/homeassistant/components/glances/translations/id.json b/homeassistant/components/glances/translations/id.json index 13127e74322..ac06b9b1962 100644 --- a/homeassistant/components/glances/translations/id.json +++ b/homeassistant/components/glances/translations/id.json @@ -4,32 +4,19 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung", - "wrong_version": "Versi tidak didukung (hanya versi 2 atau versi 3)" + "cannot_connect": "Gagal terhubung" }, "step": { "user": { "data": { "host": "Host", - "name": "Nama", "password": "Kata Sandi", "port": "Port", "ssl": "Menggunakan sertifikat SSL", "username": "Nama Pengguna", "verify_ssl": "Verifikasi sertifikat SSL", "version": "Versi API Glances (2 atau 3)" - }, - "title": "Siapkan Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frekuensi pembaruan" - }, - "description": "Konfigurasikan opsi untuk Glances" + } } } } diff --git a/homeassistant/components/glances/translations/it.json b/homeassistant/components/glances/translations/it.json index f7af778e17d..92791653d51 100644 --- a/homeassistant/components/glances/translations/it.json +++ b/homeassistant/components/glances/translations/it.json @@ -4,32 +4,19 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi", - "wrong_version": "Versione non supportata (solo 2 o 3)" + "cannot_connect": "Impossibile connettersi" }, "step": { "user": { "data": { "host": "Host", - "name": "Nome", "password": "Password", "port": "Porta", "ssl": "Utilizza un certificato SSL", "username": "Nome utente", "verify_ssl": "Verifica il certificato SSL", "version": "Glances API Version (2 o 3)" - }, - "title": "Configura Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frequenza di aggiornamento" - }, - "description": "Configura le opzioni per Glances" + } } } } diff --git a/homeassistant/components/glances/translations/ja.json b/homeassistant/components/glances/translations/ja.json index 0267110e0d0..fec577c2435 100644 --- a/homeassistant/components/glances/translations/ja.json +++ b/homeassistant/components/glances/translations/ja.json @@ -4,32 +4,19 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "wrong_version": "\u5bfe\u5fdc\u3057\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3\u306e\u307f)" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { "data": { "host": "\u30db\u30b9\u30c8", - "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b", "version": "Glances API\u30d0\u30fc\u30b8\u30e7\u30f3(2\u307e\u305f\u306f3)" - }, - "title": "Glances\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u66f4\u65b0\u983b\u5ea6" - }, - "description": "Glances\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a" + } } } } diff --git a/homeassistant/components/glances/translations/ko.json b/homeassistant/components/glances/translations/ko.json index e50206fade5..cbd2f4e2e3e 100644 --- a/homeassistant/components/glances/translations/ko.json +++ b/homeassistant/components/glances/translations/ko.json @@ -4,32 +4,19 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "wrong_version": "\ud574\ub2f9 \ubc84\uc804\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (2 \ub610\ub294 3\ub9cc \uc9c0\uc6d0)" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "name": "\uc774\ub984", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778", "version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)" - }, - "title": "Glances \uc124\uce58\ud558\uae30" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" - }, - "description": "Glances\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30" + } } } } diff --git a/homeassistant/components/glances/translations/lb.json b/homeassistant/components/glances/translations/lb.json index 68c6131cb5a..7fdd5b36583 100644 --- a/homeassistant/components/glances/translations/lb.json +++ b/homeassistant/components/glances/translations/lb.json @@ -4,32 +4,19 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { - "cannot_connect": "Feeler beim verbannen", - "wrong_version": "Versioun net \u00ebnnerst\u00ebtzt (n\u00ebmmen 2 oder 3)" + "cannot_connect": "Feeler beim verbannen" }, "step": { "user": { "data": { "host": "Apparat", - "name": "Numm", "password": "Passwuert", "port": "Port", "ssl": "Benotzt ee SSLZertifikat", "username": "Benotzernumm", "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen", "version": "API Versioun vun den Usiichten (2 oder 3)" - }, - "title": "Usiichten konfigur\u00e9ieren" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Intervalle vun de Mise \u00e0 jour" - }, - "description": "Optioune konfigur\u00e9ieren fir d'Usiichten" + } } } } diff --git a/homeassistant/components/glances/translations/nl.json b/homeassistant/components/glances/translations/nl.json index ca414a92ab9..ee2c30382d3 100644 --- a/homeassistant/components/glances/translations/nl.json +++ b/homeassistant/components/glances/translations/nl.json @@ -4,32 +4,19 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken", - "wrong_version": "Versie niet ondersteund (alleen 2 of 3)" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "user": { "data": { "host": "Host", - "name": "Naam", "password": "Wachtwoord", "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", "username": "Gebruikersnaam", "verify_ssl": "SSL-certificaat verifi\u00ebren", "version": "Glances API-versie (2 of 3)" - }, - "title": "Glances instellen" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Update frequentie" - }, - "description": "Configureer opties voor Glances" + } } } } diff --git a/homeassistant/components/glances/translations/no.json b/homeassistant/components/glances/translations/no.json index 073d764f67a..0f3d263fc5a 100644 --- a/homeassistant/components/glances/translations/no.json +++ b/homeassistant/components/glances/translations/no.json @@ -4,32 +4,19 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes", - "wrong_version": "Versjonen st\u00f8ttes ikke (bare 2 eller 3)" + "cannot_connect": "Tilkobling mislyktes" }, "step": { "user": { "data": { "host": "Vert", - "name": "Navn", "password": "Passord", "port": "Port", "ssl": "Bruker et SSL-sertifikat", "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat", "version": "Glances API-versjon (2 eller 3)" - }, - "title": "Oppsett av Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Oppdater frekvens" - }, - "description": "Konfigurasjonsalternativer for Glances" + } } } } diff --git a/homeassistant/components/glances/translations/pl.json b/homeassistant/components/glances/translations/pl.json index abef0a78208..bf69dd9f134 100644 --- a/homeassistant/components/glances/translations/pl.json +++ b/homeassistant/components/glances/translations/pl.json @@ -4,32 +4,19 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "wrong_version": "Wersja nieobs\u0142ugiwana (tylko 2 lub 3)" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { "data": { "host": "Nazwa hosta lub adres IP", - "name": "Nazwa", "password": "Has\u0142o", "port": "Port", "ssl": "Certyfikat SSL", "username": "Nazwa u\u017cytkownika", "verify_ssl": "Weryfikacja certyfikatu SSL", "version": "Glances wersja API (2 lub 3)" - }, - "title": "Konfiguracja Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" - }, - "description": "Konfiguracja opcji dla Glances" + } } } } diff --git a/homeassistant/components/glances/translations/pt-BR.json b/homeassistant/components/glances/translations/pt-BR.json index d081c897d38..2f1e45ec4dd 100644 --- a/homeassistant/components/glances/translations/pt-BR.json +++ b/homeassistant/components/glances/translations/pt-BR.json @@ -4,32 +4,19 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar", - "wrong_version": "Vers\u00e3o n\u00e3o suportada (somente 2 ou 3)" + "cannot_connect": "Falha ao conectar" }, "step": { "user": { "data": { "host": "Nome do host", - "name": "Nome", "password": "Senha", "port": "Porta", "ssl": "Usar um certificado SSL", "username": "Usu\u00e1rio", "verify_ssl": "Verifique o certificado SSL", "version": "Vers\u00e3o da API Glances (2 ou 3)" - }, - "title": "Configura\u00e7\u00e3o Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" - }, - "description": "Configure op\u00e7\u00f5es para Glances" + } } } } diff --git a/homeassistant/components/glances/translations/pt.json b/homeassistant/components/glances/translations/pt.json index 0d8cc552dd2..711e6452a8e 100644 --- a/homeassistant/components/glances/translations/pt.json +++ b/homeassistant/components/glances/translations/pt.json @@ -10,7 +10,6 @@ "user": { "data": { "host": "Servidor", - "name": "Nome", "password": "Palavra-passe", "port": "Porta", "ssl": "Utiliza um certificado SSL", @@ -19,14 +18,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/glances/translations/ru.json b/homeassistant/components/glances/translations/ru.json index aecffe204c8..52a939a85c6 100644 --- a/homeassistant/components/glances/translations/ru.json +++ b/homeassistant/components/glances/translations/ru.json @@ -4,32 +4,19 @@ "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.", - "wrong_version": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0441\u0438\u0438 2 \u0438 3." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { "data": { "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "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", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", "version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)" - }, - "title": "Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" - }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Glances" + } } } } diff --git a/homeassistant/components/glances/translations/sk.json b/homeassistant/components/glances/translations/sk.json index 39d2e182c40..ade917ee0ba 100644 --- a/homeassistant/components/glances/translations/sk.json +++ b/homeassistant/components/glances/translations/sk.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { - "name": "N\u00e1zov", - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t", + "version": "Verzia API Glances (2 alebo 3)" } } } diff --git a/homeassistant/components/glances/translations/sl.json b/homeassistant/components/glances/translations/sl.json index 081b9ebfda1..29949946ad7 100644 --- a/homeassistant/components/glances/translations/sl.json +++ b/homeassistant/components/glances/translations/sl.json @@ -4,32 +4,19 @@ "already_configured": "Gostitelj je \u017ee konfiguriran." }, "error": { - "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", - "wrong_version": "Razli\u010dica ni podprta (samo 2 ali 3)" + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem" }, "step": { "user": { "data": { "host": "Host", - "name": "Ime", "password": "Geslo", "port": "Vrata", "ssl": "Za povezavo s sistemom Glances uporabite SSL/TLS", "username": "Uporabni\u0161ko ime", "verify_ssl": "Preverite veljavnost potrdila sistema", "version": "Glances API Version (2 ali 3)" - }, - "title": "Nastavite Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Pogostost posodabljanja" - }, - "description": "Konfiguracija mo\u017enosti za Glances" + } } } } diff --git a/homeassistant/components/glances/translations/sv.json b/homeassistant/components/glances/translations/sv.json index c4ead9e6aa6..ac9afb675b3 100644 --- a/homeassistant/components/glances/translations/sv.json +++ b/homeassistant/components/glances/translations/sv.json @@ -4,32 +4,19 @@ "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." }, "error": { - "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", - "wrong_version": "Version st\u00f6ds inte (endast 2 eller 3)" + "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden" }, "step": { "user": { "data": { "host": "V\u00e4rd", - "name": "Namn", "password": "L\u00f6senord", "port": "Port", "ssl": "Anv\u00e4nd SSL / TLS f\u00f6r att ansluta till Glances-systemet", "username": "Anv\u00e4ndarnamn", "verify_ssl": "Verifiera certifieringen av systemet", "version": "Glances API-version (2 eller 3)" - }, - "title": "St\u00e4ll in Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Uppdateringsfrekvens" - }, - "description": "Konfigurera alternativ f\u00f6r Glances" + } } } } diff --git a/homeassistant/components/glances/translations/tr.json b/homeassistant/components/glances/translations/tr.json index 50b2ef9cef1..11a08d65c2a 100644 --- a/homeassistant/components/glances/translations/tr.json +++ b/homeassistant/components/glances/translations/tr.json @@ -4,32 +4,19 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "wrong_version": "S\u00fcr\u00fcm desteklenmiyor (yaln\u0131zca 2 veya 3)" + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { "user": { "data": { "host": "Sunucu", - "name": "Ad", "password": "Parola", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n", "version": "Glances API S\u00fcr\u00fcm\u00fc (2 veya 3)" - }, - "title": "Glances Kurulumu" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131" - }, - "description": "Glances i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n" + } } } } diff --git a/homeassistant/components/glances/translations/uk.json b/homeassistant/components/glances/translations/uk.json index 1fab197fe42..e1ff65cf4ab 100644 --- a/homeassistant/components/glances/translations/uk.json +++ b/homeassistant/components/glances/translations/uk.json @@ -4,32 +4,19 @@ "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." + "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", "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" + } } } } diff --git a/homeassistant/components/glances/translations/zh-Hans.json b/homeassistant/components/glances/translations/zh-Hans.json index a62b5f8b32e..55e14d47150 100644 --- a/homeassistant/components/glances/translations/zh-Hans.json +++ b/homeassistant/components/glances/translations/zh-Hans.json @@ -4,32 +4,19 @@ "already_configured": "\u8bbe\u5907\u5df2\u88ab\u8fde\u63a5" }, "error": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "wrong_version": "\u4e0d\u652f\u6301\u7684\u7248\u672c (\u4ec5\u96502\u62163)" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { "user": { "data": { "host": "\u4e3b\u673a\u5730\u5740", - "name": "\u540d\u79f0", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "ssl": "\u4f7f\u7528 SSL \u51ed\u8bc1", "username": "\u7528\u6237\u540d", "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66", "version": "Glances API \u7248\u672c (2 \u6216 3)" - }, - "title": "\u8bbe\u7f6e Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u66f4\u65b0\u9891\u7387" - }, - "description": "\u914d\u7f6e Glances \u9009\u9879" + } } } } diff --git a/homeassistant/components/glances/translations/zh-Hant.json b/homeassistant/components/glances/translations/zh-Hant.json index 3b0ddcd947a..6ea5775f099 100644 --- a/homeassistant/components/glances/translations/zh-Hant.json +++ b/homeassistant/components/glances/translations/zh-Hant.json @@ -4,32 +4,19 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "wrong_version": "\u7248\u672c\u4e0d\u652f\u63f4\uff08\u50c5 2 \u6216 3\uff09" + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", "username": "\u4f7f\u7528\u8005\u540d\u7a31", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49", "version": "Glances API \u7248\u672c\uff082 \u6216 3\uff09" - }, - "title": "\u8a2d\u5b9a Glances" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u66f4\u65b0\u983b\u7387" - }, - "description": "Glances \u8a2d\u5b9a\u9078\u9805" + } } } } diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index bb26567b8cc..67e8bca2acc 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -8,5 +8,6 @@ "codeowners": ["@tkdrob"], "quality_scale": "silver", "iot_class": "local_polling", - "loggers": ["goalzero"] + "loggers": ["goalzero"], + "integration_type": "device" } diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 345c3b41f7d..2538639883c 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="timeToEmptyFull", name="Time to empty/full", - device_class=TIME_MINUTES, + device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=TIME_MINUTES, ), SensorEntityDescription( diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index d41a2238854..3d551367e73 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -19,7 +19,7 @@ "host": "Host", "name": "Name" }, - "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind." + "description": "Bitte lese die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind." } } } diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json index 7bd8c3df311..ed028936aa3 100644 --- a/homeassistant/components/goalzero/translations/ru.json +++ b/homeassistant/components/goalzero/translations/ru.json @@ -2,12 +2,12 @@ "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.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\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." }, "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_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\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." }, "step": { diff --git a/homeassistant/components/goalzero/translations/sk.json b/homeassistant/components/goalzero/translations/sk.json index af15f92c2f2..dc9ffc47876 100644 --- a/homeassistant/components/goalzero/translations/sk.json +++ b/homeassistant/components/goalzero/translations/sk.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov" - } + }, + "description": "Pozrite si dokument\u00e1ciu, aby ste sa uistili, \u017ee s\u00fa splnen\u00e9 v\u0161etky po\u017eiadavky." } } } diff --git a/homeassistant/components/gogogate2/translations/sk.json b/homeassistant/components/gogogate2/translations/sk.json index 5ada995aa6e..b704eb5be7f 100644 --- a/homeassistant/components/gogogate2/translations/sk.json +++ b/homeassistant/components/gogogate2/translations/sk.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "flow_title": "{device} ({ip_address})", + "step": { + "user": { + "data": { + "ip_address": "IP adresa", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Ni\u017e\u0161ie uve\u010fte po\u017eadovan\u00e9 inform\u00e1cie." + } } } } \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/de.json b/homeassistant/components/goodwe/translations/de.json index 2eac1136e3c..7a2e04e2aaa 100644 --- a/homeassistant/components/goodwe/translations/de.json +++ b/homeassistant/components/goodwe/translations/de.json @@ -13,7 +13,7 @@ "host": "IP-Adresse" }, "description": "Mit Wechselrichter verbinden", - "title": "GoodWe-Wechselrichter" + "title": "GoodWe Wechselrichter" } } } diff --git a/homeassistant/components/goodwe/translations/sk.json b/homeassistant/components/goodwe/translations/sk.json index bee0999420f..9a7539ecc3b 100644 --- a/homeassistant/components/goodwe/translations/sk.json +++ b/homeassistant/components/goodwe/translations/sk.json @@ -1,7 +1,19 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "error": { + "connection_error": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "host": "IP adresa" + }, + "description": "Pripoji\u0165 k meni\u010du" + } } } } \ No newline at end of file diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index bc6c719c8fd..542eb72206a 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==4.0.3", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.4", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/homeassistant/components/google/translations/bg.json b/homeassistant/components/google/translations/bg.json index 2fa49447827..7d8e0d3083e 100644 --- a/homeassistant/components/google/translations/bg.json +++ b/homeassistant/components/google/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u043e" @@ -18,11 +18,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Google Calendar \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index b7645abe54b..066630df50d 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "La configuraci\u00f3 de Google Calentdar mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent de credencials d'aplicaci\u00f3 OAuth i d'acc\u00e9s s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "La configuraci\u00f3 YAML de Google Calendar est\u00e0 sent eliminada" - }, - "removed_track_new_yaml": { - "description": "Has desactivat el seguiment d'entitats de Google Calendar a configuration.yaml, que ja no \u00e9s compatible. Per desactivar les entitats descobertes recentment, a partir d'ara, has de canviar manualment les opcions de sistema de la integraci\u00f3 a trav\u00e9s de la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 'track_new' de configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "El seguiment d'entitats de Google Calendar ha canviat" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/cs.json b/homeassistant/components/google/translations/cs.json index f1d2cc4c85a..039106b4e21 100644 --- a/homeassistant/components/google/translations/cs.json +++ b/homeassistant/components/google/translations/cs.json @@ -7,7 +7,8 @@ "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "oauth_error": "P\u0159ijata neplatn\u00e1 data tokenu.", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed" }, "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 377cafe035e..6569dbce676 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Die Konfiguration des Google Kalenders in configuration.yaml wird in Home Assistant 2022.9 entfernt. \n\nDeine bestehenden OAuth-Anwendungsdaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Google Calendar YAML-Konfiguration wird entfernt" - }, - "removed_track_new_yaml": { - "description": "Du hast die Entit\u00e4tsverfolgung f\u00fcr Google Kalender in configuration.yaml deaktiviert, was nicht mehr unterst\u00fctzt wird. Du musst die Integrationssystemoptionen in der Benutzeroberfl\u00e4che manuell \u00e4ndern, um neu entdeckte Entit\u00e4ten in Zukunft zu deaktivieren. Entferne die Einstellung track_new aus configuration.yaml und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Google Calendar Entity Tracking hat sich ge\u00e4ndert" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 0bf592d60d4..21e5580da3d 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - }, - "removed_track_new_yaml": { - "description": "\u0388\u03c7\u03b5\u03c4\u03b5 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf Google \u03c3\u03c4\u03bf configuration.yaml, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03c4\u03b9\u03c2 \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 track_new \u03b1\u03c0\u03cc \u03c4\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 4ce207ccd5b..1720e8c1454 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Google Calendar YAML configuration is being removed" - }, - "removed_track_new_yaml": { - "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", - "title": "Google Calendar entity tracking has changed" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 107a320eb30..64b54f81064 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de Google Calendar en configuration.yaml en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la configuraci\u00f3n YAML de Google Calendar" - }, - "removed_track_new_yaml": { - "description": "Has deshabilitado el rastreo de entidades para Google Calendar en configuration.yaml, que ya no es compatible. Debes cambiar manualmente las opciones de sistema de la integraci\u00f3n en la IU para deshabilitar las entidades reci\u00e9n descubiertas en el futuro. Elimina la configuraci\u00f3n track_new de configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "El rastreo de entidades de Google Calendar ha cambiado" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index 83dda4e151c..c516c9201e2 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Google'i kalendri konfigureerimine failis configuration.yaml eemaldatakse versioonis Home Assistant 2022.9.\n\nTeie olemasolevad OAuth-rakenduse volitused ja juurdep\u00e4\u00e4su seaded on automaatselt kasutajaliidesesse imporditud. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", - "title": "Google'i kalendri YAML-i konfiguratsioon eemaldatakse" - }, - "removed_track_new_yaml": { - "description": "Oled keelanud Google'i kalendri olemite j\u00e4lgimise rakenduses configuration.yaml, mida enam ei toetata. Peate kasutajaliideses integratsioonis\u00fcsteemi suvandeid k\u00e4sitsi muutma, et \u00e4sja avastatud olemid edaspidi keelata. Eemaldage saidilt configuration.yaml s\u00e4te track_new ja taask\u00e4ivitage home assistant selle probleemi lahendamiseks.", - "title": "Google'i kalendri olemi j\u00e4lgimine on muutunud" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index b404fbb29be..389f769cdb9 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -33,11 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "title": "La configuration YAML pour Google\u00a0Agenda sera bient\u00f4t supprim\u00e9e" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index 467b1a663f2..b27e06b15c7 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "A Google Napt\u00e1r konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jlban a 2022.9-es Home Assistant verzi\u00f3ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 OAuth alkalmaz\u00e1s hiteles\u00edt\u0151 adatai \u00e9s hozz\u00e1f\u00e9r\u00e9si be\u00e1ll\u00edt\u00e1sai automatikusan import\u00e1l\u00e1sra ker\u00fcltek a felhaszn\u00e1l\u00f3i fel\u00fcletbe. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Google Calendar YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - }, - "removed_track_new_yaml": { - "description": "A configuration.yaml f\u00e1jlban a Google Calendar sz\u00e1m\u00e1ra az entit\u00e1sk\u00f6vet\u00e9s ki lett kapcsolva, ami m\u00e1r nem t\u00e1mogatott. Manu\u00e1lisan sz\u00fcks\u00e9ges m\u00f3dos\u00edtani az integr\u00e1ci\u00f3s rendszerbe\u00e1ll\u00edt\u00e1sokat a felhaszn\u00e1l\u00f3i fel\u00fcleten, hogy a j\u00f6v\u0151ben letiltsa az \u00fajonnan felfedezett entit\u00e1sokat. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a track_new be\u00e1ll\u00edt\u00e1st a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Google Napt\u00e1r entit\u00e1sk\u00f6vet\u00e9se megv\u00e1ltozott" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index 0488e5abbeb..20ed21a56be 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Proses konfigurasi Integrasi Google Kalender di configuration.yaml dalam proses penghapusan di Home Assistant 2022.9.\n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Integrasi Google Kalender dalam proses penghapusan" - }, - "removed_track_new_yaml": { - "description": "Anda telah menonaktifkan pelacakan entitas untuk Google Kalender di configuration.yaml, yang kini tidak lagi didukung. Anda harus secara manual mengubah Opsi Sistem integrasi di antarmuka untuk menonaktifkan entitas yang baru ditemukan di masa datang. Hapus pengaturan track_new dari configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Pelacakan entitas Google Kalender telah berubah" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index 282c1d06544..782fed55d5b 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "La configurazione di Google Calendar in configuration.yaml sar\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Google Calendar sar\u00e0 rimossa" - }, - "removed_track_new_yaml": { - "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le nuove entit\u00e0 rilevate in futuro. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "Il tracciamento dell'entit\u00e0 di Google Calendar \u00e8 cambiato" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 057734a2bca..6e2aac00c5d 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u306a\u304a\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - }, - "removed_track_new_yaml": { - "description": "configuration.yaml\u3067\u3001Google \u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u8ffd\u8de1\u3092\u7121\u52b9\u306b\u3067\u304d\u307e\u3057\u305f\u304c\u3001\u3053\u308c\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002UI\u306e\u7d71\u5408\u30b7\u30b9\u30c6\u30e0\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u5909\u66f4\u3057\u3066\u3001\u65b0\u3057\u304f\u691c\u51fa\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4eca\u5f8c\u7121\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059(disable newly discovered entities going forward)\u3002configuration.yaml\u304b\u3089track_new\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3057\u307e\u3059\u3002", - "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json index 572ac985041..05b053497c4 100644 --- a/homeassistant/components/google/translations/nl.json +++ b/homeassistant/components/google/translations/nl.json @@ -30,11 +30,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "title": "De Google Calendar YAML-configuratie wordt verwijderd" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json index d020a0f294e..cbc39b8ff6a 100644 --- a/homeassistant/components/google/translations/no.json +++ b/homeassistant/components/google/translations/no.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Google Kalender i configuration.yaml blir fjernet i Home Assistant 2022.9. \n\n Din eksisterende OAuth-applikasjonslegitimasjon og tilgangsinnstillinger er automatisk importert til brukergrensesnittet. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "Google Kalender YAML-konfigurasjonen blir fjernet" - }, - "removed_track_new_yaml": { - "description": "Du har deaktivert enhetssporing for Google Kalender i configuration.yaml, som ikke lenger st\u00f8ttes. Du m\u00e5 manuelt endre integreringssystemalternativene i brukergrensesnittet for \u00e5 deaktivere nyoppdagede enheter fremover. Fjern track_new-innstillingen fra configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "Google Kalender-enhetssporing er endret" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index fb85af430df..ff7b8af3bfc 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Konfiguracja Kalendarza Google w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Konfiguracja YAML dla Kalendarza Google zostanie usuni\u0119ta" - }, - "removed_track_new_yaml": { - "description": "Wy\u0142\u0105czy\u0142e\u015b \u015bledzenie encji w Kalendarzu Google w pliku configuration.yaml, kt\u00f3ry nie jest ju\u017c obs\u0142ugiwany. Musisz r\u0119cznie zmieni\u0107 ustawienie w Opcjach Systemu integracji, aby wy\u0142\u0105czy\u0107 nowo wykryte encje w przysz\u0142o\u015bci. Usu\u0144 ustawienie track_new z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "\u015aledzenie encji Kalendarza Google uleg\u0142o zmianie" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 709737dbe2d..c7068a94602 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o do Google Agenda em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o YAML do Google Agenda est\u00e1 sendo removida" - }, - "removed_track_new_yaml": { - "description": "Voc\u00ea desativou as entidades de rastreamento para o Google Agenda em configuration.yaml, que n\u00e3o \u00e9 mais compat\u00edvel. Voc\u00ea deve alterar manualmente as op\u00e7\u00f5es do sistema de integra\u00e7\u00e3o na interface do usu\u00e1rio para desativar as entidades rec\u00e9m-descobertas daqui para frente. Remova a configura\u00e7\u00e3o track_new de configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A entidade de rastreamento do Google Agenda foi alterado" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index be7b92a707c..5fc2cb03feb 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - }, - "removed_track_new_yaml": { - "description": "\u0412\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u043b\u0438 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f Google Calendar \u0432 \u0444\u0430\u0439\u043b\u0435 configuration.yaml, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0427\u0442\u043e\u0431\u044b \u043d\u043e\u0432\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u043b\u0438\u0441\u044c \u0432 Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 track_new \u0438\u0437 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 Google Calendar" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/sk.json b/homeassistant/components/google/translations/sk.json new file mode 100644 index 00000000000..01103448bea --- /dev/null +++ b/homeassistant/components/google/translations/sk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "progress": { + "exchange": "Ak chcete prepoji\u0165 svoj \u00fa\u010det Google, nav\u0161t\u00edvte [{url}]({url}) a zadajte k\u00f3d: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "Prepojenie konta Google" + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Pr\u00edstup Home Assistant ku kalend\u00e1ru Google" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/sv.json b/homeassistant/components/google/translations/sv.json index 499ea375547..110ecd8773a 100644 --- a/homeassistant/components/google/translations/sv.json +++ b/homeassistant/components/google/translations/sv.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Google Kalender i configuration.yaml tas bort i Home Assistant 2022.9. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har importerats till anv\u00e4ndargr\u00e4nssnittet automatiskt. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Google Kalender YAML-konfigurationen tas bort" - }, - "removed_track_new_yaml": { - "description": "Du har inaktiverat enhetssp\u00e5rning f\u00f6r Google Kalender i configuration.yaml, som inte l\u00e4ngre st\u00f6ds. Du m\u00e5ste manuellt \u00e4ndra integrationssystemalternativen i anv\u00e4ndargr\u00e4nssnittet f\u00f6r att inaktivera nyuppt\u00e4ckta enheter fram\u00f6ver. Ta bort inst\u00e4llningen track_new fr\u00e5n configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Sp\u00e5rning av enheter i Google Kalender har \u00e4ndrats" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index a7074b69127..7d67018630f 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "Google Takvim'in configuration.yaml dosyas\u0131nda yap\u0131land\u0131r\u0131lmas\u0131 Home Assistant 2022.9'da kald\u0131r\u0131l\u0131yor.\n\nMevcut OAuth Uygulama Kimlik Bilgileriniz ve eri\u015fim ayarlar\u0131n\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131lm\u0131\u015ft\u0131r. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "Google Takvim YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" - }, - "removed_track_new_yaml": { - "description": "Art\u0131k desteklenmeyen configuration.yaml dosyas\u0131nda Google Takvim i\u00e7in varl\u0131k izlemeyi devre d\u0131\u015f\u0131 b\u0131rakt\u0131n\u0131z. \u0130leride yeni ke\u015ffedilen varl\u0131klar\u0131 devre d\u0131\u015f\u0131 b\u0131rakmak i\u00e7in kullan\u0131c\u0131 aray\u00fcz\u00fcndeki entegrasyon Sistem Se\u00e7eneklerini manuel olarak de\u011fi\u015ftirmeniz gerekir. Bu sorunu \u00e7\u00f6zmek i\u00e7in configuration.yaml dosyas\u0131ndan track_new ayar\u0131n\u0131 kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "Google Takvim varl\u0131k takibi de\u011fi\u015fti" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 93e2fa8f7ba..45dc83c1b3c 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -33,16 +33,6 @@ } } }, - "issues": { - "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" - }, - "removed_track_new_yaml": { - "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u5df2\u7d93\u8b8a\u66f4" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/google_sheets/manifest.json b/homeassistant/components/google_sheets/manifest.json index c8d86210b42..1c7790b1f25 100644 --- a/homeassistant/components/google_sheets/manifest.json +++ b/homeassistant/components/google_sheets/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_sheets/", "requirements": ["gspread==5.5.0"], "codeowners": ["@tkdrob"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "integration_type": "service" } diff --git a/homeassistant/components/google_sheets/translations/bg.json b/homeassistant/components/google_sheets/translations/bg.json index 80ba164940b..ec743b0b8fd 100644 --- a/homeassistant/components/google_sheets/translations/bg.json +++ b/homeassistant/components/google_sheets/translations/bg.json @@ -5,7 +5,7 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "create_spreadsheet_failure": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0430, \u0432\u0438\u0436\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b\u0430 \u0437\u0430 \u0433\u0440\u0435\u0448\u043a\u0438 \u0437\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "create_entry": { diff --git a/homeassistant/components/google_sheets/translations/cs.json b/homeassistant/components/google_sheets/translations/cs.json new file mode 100644 index 00000000000..1ce33bab1a9 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "create_spreadsheet_failure": "P\u0159i vytv\u00e1\u0159en\u00ed tabulky do\u0161lo k chyb\u011b, podrobnosti naleznete v protokolu chyb", + "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "oauth_error": "P\u0159ijata neplatn\u00e1 data tokenu.", + "open_spreadsheet_failure": "Chyba p\u0159i otev\u00edr\u00e1n\u00ed tabulky, podrobnosti naleznete v protokolu chyb", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno a vytvo\u0159ena tabulka na adrese: {url}" + }, + "step": { + "auth": { + "title": "Propojit \u00fa\u010det Google" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/nl.json b/homeassistant/components/google_sheets/translations/nl.json index d530b4e3add..884233629ea 100644 --- a/homeassistant/components/google_sheets/translations/nl.json +++ b/homeassistant/components/google_sheets/translations/nl.json @@ -4,13 +4,18 @@ "already_configured": "Account is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", + "create_spreadsheet_failure": "Fout bij het openen van een nieuw werkblad, zie de error log voor details", "invalid_access_token": "Ongeldig toegangstoken", "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", "oauth_error": "Ongeldige tokengegevens ontvangen.", + "open_spreadsheet_failure": "Fout bij het openen van een nieuw werkblad, zie de error log voor details", "reauth_successful": "Herauthenticatie geslaagd", "timeout_connect": "Time-out bij het maken van verbinding", "unknown": "Onverwachte fout" }, + "create_entry": { + "default": "Succesvol aangemeld en werkblad gemaakt op: {url}" + }, "step": { "auth": { "title": "Google-account koppelen" @@ -19,6 +24,7 @@ "title": "Kies een authenticatie methode" }, "reauth_confirm": { + "description": "De Google Sheets integratie vereist dat je opnieuw inlogt met je account", "title": "Integratie herauthenticeren" } } diff --git a/homeassistant/components/google_sheets/translations/sk.json b/homeassistant/components/google_sheets/translations/sk.json new file mode 100644 index 00000000000..37e824e8df9 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/sk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "create_spreadsheet_failure": "Chyba pri vytv\u00e1ran\u00ed tabu\u013eky, podrobnosti n\u00e1jdete v protokole ch\u00fdb", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu.", + "open_spreadsheet_failure": "Chyba pri otv\u00e1ran\u00ed tabu\u013eky, podrobnosti n\u00e1jdete v protokole ch\u00fdb", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9 a tabu\u013eka vytvoren\u00e1 na adrese: {url}" + }, + "step": { + "auth": { + "title": "Prepoji\u0165 \u00fa\u010det Google" + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "description": "Integr\u00e1cia Tabuliek Google vy\u017eaduje op\u00e4tovn\u00e9 overenie v\u00e1\u0161ho \u00fa\u010dtu", + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json index 3965f9c706c..b599c0d4ded 100644 --- a/homeassistant/components/google_travel_time/translations/bg.json +++ b/homeassistant/components/google_travel_time/translations/bg.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "user": { diff --git a/homeassistant/components/google_travel_time/translations/ca.json b/homeassistant/components/google_travel_time/translations/ca.json index cfd24ef02a8..34ca784b3ec 100644 --- a/homeassistant/components/google_travel_time/translations/ca.json +++ b/homeassistant/components/google_travel_time/translations/ca.json @@ -28,6 +28,7 @@ "mode": "Mode de transport", "time": "Temps", "time_type": "Tipus de temps", + "traffic_mode": "Mode tr\u00e0nsit", "transit_mode": "Tipus de transport", "transit_routing_preference": "Prefer\u00e8ncia de rutes de tr\u00e0nsit", "units": "Unitats" diff --git a/homeassistant/components/google_travel_time/translations/cs.json b/homeassistant/components/google_travel_time/translations/cs.json index 19da83d1596..edfdda6d4df 100644 --- a/homeassistant/components/google_travel_time/translations/cs.json +++ b/homeassistant/components/google_travel_time/translations/cs.json @@ -4,7 +4,8 @@ "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { "user": { diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json index 24bf9799ee0..c35d258b137 100644 --- a/homeassistant/components/google_travel_time/translations/de.json +++ b/homeassistant/components/google_travel_time/translations/de.json @@ -15,7 +15,7 @@ "name": "Name", "origin": "Startort" }, - "description": "Bei der Angabe von Ursprung und Ziel kannst du einen oder mehrere durch das Pipe-Zeichen getrennte Orte in Form einer Adresse, L\u00e4ngen- / Breitengradkoordinaten oder einer Google-Orts-ID angeben. Wenn du den Standort mithilfe einer Google-Orts-ID angibst, muss der ID \"place_id:\" vorangestellt werden." + "description": "Bei der Angabe von Ursprung und Ziel kannst du einen oder mehrere durch das Pipe-Zeichen getrennte Orte in Form einer Adresse, L\u00e4ngen- / Breitengradkoordinaten oder einer Google-Orts-ID angeben. Wenn du den Standort mithilfe einer Google-Orts-ID angibst, muss der ID 'place_id:' vorangestellt werden." } } }, @@ -28,6 +28,7 @@ "mode": "Reisemodus", "time": "Uhrzeit", "time_type": "Zeittyp", + "traffic_mode": "Verkehrsmodus", "transit_mode": "Transit-Modus", "transit_routing_preference": "Transit-Routing-Einstellungen", "units": "Einheiten" diff --git a/homeassistant/components/google_travel_time/translations/el.json b/homeassistant/components/google_travel_time/translations/el.json index 8180ef61c03..c7e821b6b39 100644 --- a/homeassistant/components/google_travel_time/translations/el.json +++ b/homeassistant/components/google_travel_time/translations/el.json @@ -4,7 +4,8 @@ "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { "user": { @@ -27,6 +28,7 @@ "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03b1\u03be\u03b9\u03b4\u03b9\u03bf\u03cd", "time": "\u038f\u03c1\u03b1", "time_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ce\u03c1\u03b1\u03c2", + "traffic_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03c5\u03ba\u03bb\u03bf\u03c6\u03bf\u03c1\u03af\u03b1\u03c2", "transit_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03c5\u03b3\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2", "transit_routing_preference": "\u03a0\u03c1\u03bf\u03c4\u03af\u03bc\u03b7\u03c3\u03b7 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c5\u03b3\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2", "units": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b5\u03c2" diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json index 54c8320665c..508aee06b7b 100644 --- a/homeassistant/components/google_travel_time/translations/es.json +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -28,6 +28,7 @@ "mode": "Modo de viaje", "time": "Hora", "time_type": "Tipo de tiempo", + "traffic_mode": "Modo de tr\u00e1fico", "transit_mode": "Modo de tr\u00e1nsito", "transit_routing_preference": "Preferencia de ruta de tr\u00e1nsito", "units": "Unidades" diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json index 0c8a90e8949..27adfb1e1b9 100644 --- a/homeassistant/components/google_travel_time/translations/et.json +++ b/homeassistant/components/google_travel_time/translations/et.json @@ -28,6 +28,7 @@ "mode": "Reisimise viis", "time": "Aeg", "time_type": "Aja t\u00fc\u00fcp", + "traffic_mode": "S\u00f5iduvahend", "transit_mode": "Liikumisviis", "transit_routing_preference": "Teekonna eelistused", "units": "\u00dchikud" diff --git a/homeassistant/components/google_travel_time/translations/he.json b/homeassistant/components/google_travel_time/translations/he.json index b10c8890fd4..3f8d7bd1257 100644 --- a/homeassistant/components/google_travel_time/translations/he.json +++ b/homeassistant/components/google_travel_time/translations/he.json @@ -24,6 +24,7 @@ "data": { "language": "\u05e9\u05e4\u05d4", "time": "\u05d6\u05de\u05df", + "traffic_mode": "\u05de\u05e6\u05d1 \u05ea\u05e0\u05d5\u05e2\u05d4", "units": "\u05d9\u05d7\u05d9\u05d3\u05d5\u05ea" } } diff --git a/homeassistant/components/google_travel_time/translations/hr.json b/homeassistant/components/google_travel_time/translations/hr.json new file mode 100644 index 00000000000..6e71d340dc1 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neva\u017ee\u0107a provjera autenti\u010dnosti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json index 3ad294f4fab..73d55885854 100644 --- a/homeassistant/components/google_travel_time/translations/hu.json +++ b/homeassistant/components/google_travel_time/translations/hu.json @@ -28,6 +28,7 @@ "mode": "Utaz\u00e1si m\u00f3d", "time": "Id\u0151", "time_type": "Id\u0151 t\u00edpusa", + "traffic_mode": "Forgalmi m\u00f3d", "transit_mode": "Tranzit m\u00f3d", "transit_routing_preference": "Tranzit \u00fatv\u00e1laszt\u00e1si be\u00e1ll\u00edt\u00e1s", "units": "Egys\u00e9gek" diff --git a/homeassistant/components/google_travel_time/translations/id.json b/homeassistant/components/google_travel_time/translations/id.json index e960d03c341..bcfbafae4b7 100644 --- a/homeassistant/components/google_travel_time/translations/id.json +++ b/homeassistant/components/google_travel_time/translations/id.json @@ -4,7 +4,8 @@ "already_configured": "Lokasi sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" }, "step": { "user": { @@ -27,6 +28,7 @@ "mode": "Mode Perjalanan", "time": "Waktu", "time_type": "Jenis Waktu", + "traffic_mode": "Mode Lalu Lintas", "transit_mode": "Mode Transit", "transit_routing_preference": "Preferensi Perutean Transit", "units": "Unit" diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json index 16dc9f85f66..8648bf33d6d 100644 --- a/homeassistant/components/google_travel_time/translations/it.json +++ b/homeassistant/components/google_travel_time/translations/it.json @@ -28,6 +28,7 @@ "mode": "Modalit\u00e0 di viaggio", "time": "Ora", "time_type": "Tipo di ora", + "traffic_mode": "Modalit\u00e0 traffico", "transit_mode": "Modalit\u00e0 di transito", "transit_routing_preference": "Preferenza percorso di transito", "units": "Unit\u00e0" diff --git a/homeassistant/components/google_travel_time/translations/no.json b/homeassistant/components/google_travel_time/translations/no.json index 2a056451bbe..0a9cb3e6d83 100644 --- a/homeassistant/components/google_travel_time/translations/no.json +++ b/homeassistant/components/google_travel_time/translations/no.json @@ -28,6 +28,7 @@ "mode": "Reisemodus", "time": "Tid", "time_type": "Tidstype", + "traffic_mode": "Trafikkmodus", "transit_mode": "Transittmodus", "transit_routing_preference": "Ruteinnstillinger for kollektivtransport", "units": "Enheter" diff --git a/homeassistant/components/google_travel_time/translations/pl.json b/homeassistant/components/google_travel_time/translations/pl.json index 0890a4fd350..1ced0b912e6 100644 --- a/homeassistant/components/google_travel_time/translations/pl.json +++ b/homeassistant/components/google_travel_time/translations/pl.json @@ -28,6 +28,7 @@ "mode": "Tryb podr\u00f3\u017cy", "time": "Czas", "time_type": "Typ czasu", + "traffic_mode": "Tryb nat\u0119\u017cenia ruchu", "transit_mode": "Tryb tranzytu", "transit_routing_preference": "Preferencje trasy tranzytowej", "units": "Jednostki" diff --git a/homeassistant/components/google_travel_time/translations/pt-BR.json b/homeassistant/components/google_travel_time/translations/pt-BR.json index 9b8249b1b51..baa16f38f35 100644 --- a/homeassistant/components/google_travel_time/translations/pt-BR.json +++ b/homeassistant/components/google_travel_time/translations/pt-BR.json @@ -28,6 +28,7 @@ "mode": "Modo de viagem", "time": "Tempo", "time_type": "Tipo de tempo", + "traffic_mode": "Modo de tr\u00e1fego", "transit_mode": "Modo de tr\u00e2nsito", "transit_routing_preference": "Prefer\u00eancia de rota de tr\u00e2nsito", "units": "Unidades" diff --git a/homeassistant/components/google_travel_time/translations/ru.json b/homeassistant/components/google_travel_time/translations/ru.json index d506ed4ca5e..31494ff1c1d 100644 --- a/homeassistant/components/google_travel_time/translations/ru.json +++ b/homeassistant/components/google_travel_time/translations/ru.json @@ -28,6 +28,7 @@ "mode": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f", "time": "\u0412\u0440\u0435\u043c\u044f", "time_type": "\u0422\u0438\u043f \u0432\u0440\u0435\u043c\u0435\u043d\u0438", + "traffic_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u0444\u0438\u043a\u0430", "transit_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u0430", "transit_routing_preference": "\u041f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043f\u043e \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u043d\u043e\u043c\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0443", "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f" diff --git a/homeassistant/components/google_travel_time/translations/sk.json b/homeassistant/components/google_travel_time/translations/sk.json index 52d93a1a18e..233c48e1b42 100644 --- a/homeassistant/components/google_travel_time/translations/sk.json +++ b/homeassistant/components/google_travel_time/translations/sk.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie" + }, "step": { "user": { "data": { @@ -11,5 +15,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Jazyk", + "time": "\u010cas", + "units": "Jednotky" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json index 29bf50f3376..a964f0a09f1 100644 --- a/homeassistant/components/google_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json @@ -28,6 +28,7 @@ "mode": "\u65c5\u884c\u6a21\u5f0f", "time": "\u6642\u9593", "time_type": "\u6642\u9593\u985e\u5225", + "traffic_mode": "\u4ea4\u901a\u6a21\u5f0f", "transit_mode": "\u79fb\u52d5\u6a21\u5f0f", "transit_routing_preference": "\u504f\u597d\u79fb\u52d5\u8def\u7dda", "units": "\u55ae\u4f4d" diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index 4faa6befa06..1d193287419 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from govee_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from govee_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -20,16 +20,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN @@ -72,27 +69,13 @@ def _device_key_to_bluetooth_entity_key( return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to hass device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/govee_ble/translations/he.json b/homeassistant/components/govee_ble/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/govee_ble/translations/he.json +++ b/homeassistant/components/govee_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/govee_ble/translations/sk.json b/homeassistant/components/govee_ble/translations/sk.json new file mode 100644 index 00000000000..b121bbc35a3 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index f18b486917a..a452d32e544 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,6 +1,5 @@ """Support for the GPSLogger device tracking.""" -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/gpslogger/translations/sk.json b/homeassistant/components/gpslogger/translations/sk.json new file mode 100644 index 00000000000..933f73976d2 --- /dev/null +++ b/homeassistant/components/gpslogger/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/he.json b/homeassistant/components/gree/translations/he.json index d3d68dccc93..4eafc6dc29b 100644 --- a/homeassistant/components/gree/translations/he.json +++ b/homeassistant/components/gree/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/gree/translations/sk.json b/homeassistant/components/gree/translations/sk.json new file mode 100644 index 00000000000..d4bb209c34c --- /dev/null +++ b/homeassistant/components/gree/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 473a5a5e885..815e3b76f0b 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -94,7 +95,7 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): self, unique_id: str | None, name: str, - device_class: str | None, + device_class: BinarySensorDeviceClass | None, entity_ids: list[str], mode: str | None, ) -> None: @@ -149,6 +150,6 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): self._attr_is_on = self.mode(state == STATE_ON for state in states) @property - def device_class(self) -> str | None: + def device_class(self) -> BinarySensorDeviceClass | None: """Return the sensor class of the binary sensor.""" return self._device_class diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index f47dbcb3b44..9a084cde685 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Group integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Callable, Coroutine, Mapping from functools import partial from typing import Any, cast @@ -11,6 +11,7 @@ from homeassistant.const import CONF_ENTITIES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, SchemaConfigFlowHandler, SchemaFlowFormStep, SchemaFlowMenuStep, @@ -23,17 +24,15 @@ from .binary_sensor import CONF_ALL from .const import CONF_HIDE_MEMBERS -def basic_group_options_schema( - domain: str, - handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, - options: dict[str, Any], +async def basic_group_options_schema( + domain: str, handler: SchemaCommonFlowHandler ) -> vol.Schema: """Generate options schema.""" - handler = cast(SchemaOptionsFlowHandler, handler) return vol.Schema( { vol.Required(CONF_ENTITIES): entity_selector_without_own_entities( - handler, selector.EntitySelectorConfig(domain=domain, multiple=True) + cast(SchemaOptionsFlowHandler, handler.parent_handler), + selector.EntitySelectorConfig(domain=domain, multiple=True), ), vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(), } @@ -53,12 +52,9 @@ def basic_group_config_schema(domain: str) -> vol.Schema: ) -def binary_sensor_options_schema( - handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, - options: dict[str, Any], -) -> vol.Schema: +async def binary_sensor_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: """Generate options schema.""" - return basic_group_options_schema("binary_sensor", handler, options).extend( + return (await basic_group_options_schema("binary_sensor", handler)).extend( { vol.Required(CONF_ALL, default=False): selector.BooleanSelector(), } @@ -72,13 +68,11 @@ BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend( ) -def light_switch_options_schema( - domain: str, - handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, - options: dict[str, Any], +async def light_switch_options_schema( + domain: str, handler: SchemaCommonFlowHandler ) -> vol.Schema: """Generate options schema.""" - return basic_group_options_schema(domain, handler, options).extend( + return (await basic_group_options_schema(domain, handler)).extend( { vol.Required( CONF_ALL, default=False, description={"advanced": True} @@ -98,49 +92,62 @@ GROUP_TYPES = [ ] -@callback -def choose_options_step(options: dict[str, Any]) -> str: +async def choose_options_step(options: dict[str, Any]) -> str: """Return next step_id for options flow according to group_type.""" return cast(str, options["group_type"]) -def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]]: +def set_group_type( + group_type: str, +) -> Callable[ + [SchemaCommonFlowHandler, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]] +]: """Set group type.""" - @callback - def _set_group_type(user_input: dict[str, Any]) -> dict[str, Any]: + async def _set_group_type( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] + ) -> dict[str, Any]: """Add group type to user input.""" return {"group_type": group_type, **user_input} return _set_group_type -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { +CONFIG_FLOW = { "user": SchemaFlowMenuStep(GROUP_TYPES), "binary_sensor": SchemaFlowFormStep( - BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor") + BINARY_SENSOR_CONFIG_SCHEMA, + validate_user_input=set_group_type("binary_sensor"), ), "cover": SchemaFlowFormStep( - basic_group_config_schema("cover"), set_group_type("cover") + basic_group_config_schema("cover"), + validate_user_input=set_group_type("cover"), + ), + "fan": SchemaFlowFormStep( + basic_group_config_schema("fan"), + validate_user_input=set_group_type("fan"), ), - "fan": SchemaFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")), "light": SchemaFlowFormStep( - basic_group_config_schema("light"), set_group_type("light") + basic_group_config_schema("light"), + validate_user_input=set_group_type("light"), ), "lock": SchemaFlowFormStep( - basic_group_config_schema("lock"), set_group_type("lock") + basic_group_config_schema("lock"), + validate_user_input=set_group_type("lock"), ), "media_player": SchemaFlowFormStep( - basic_group_config_schema("media_player"), set_group_type("media_player") + basic_group_config_schema("media_player"), + validate_user_input=set_group_type("media_player"), ), "switch": SchemaFlowFormStep( - basic_group_config_schema("switch"), set_group_type("switch") + basic_group_config_schema("switch"), + validate_user_input=set_group_type("switch"), ), } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(None, next_step=choose_options_step), +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(next_step=choose_options_step), "binary_sensor": SchemaFlowFormStep(binary_sensor_options_schema), "cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), "fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index a867c92d956..2ecfbeaca42 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -324,7 +324,7 @@ class CoverGroup(GroupEntity, CoverEntity): tilt_states, ATTR_CURRENT_TILT_POSITION ) - supported_features = 0 + supported_features = CoverEntityFeature(0) if self._covers[KEY_OPEN_CLOSE]: supported_features |= CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE supported_features |= CoverEntityFeature.STOP if self._covers[KEY_STOP] else 0 diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 7d09c9573b5..682890dddd6 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -110,18 +110,12 @@ class FanGroup(GroupEntity, FanEntity): self._percentage = None self._oscillating = None self._direction = None - self._supported_features = 0 self._speed_count = 100 self._is_on: bool | None = False self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities} self._attr_unique_id = unique_id - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" @@ -319,8 +313,10 @@ class FanGroup(GroupEntity, FanEntity): "_direction", FanEntityFeature.DIRECTION, ATTR_DIRECTION ) - self._supported_features = reduce( - ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0 + self._attr_supported_features = FanEntityFeature( + reduce( + ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0 + ) ) self._attr_assumed_state |= any( state.attributes.get(ATTR_ASSUMED_STATE) for state in states diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 71afa5d104b..6315e79d61a 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -287,7 +287,7 @@ class LightGroup(GroupEntity, LightEntity): set[str], set().union(*all_supported_color_modes) ) - self._attr_supported_features = 0 + self._attr_supported_features = LightEntityFeature(0) for support in find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature # we find. diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index ddb44072080..a349a628004 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -1,6 +1,7 @@ """This platform allows several media players to be grouped into one media player.""" from __future__ import annotations +from contextlib import suppress from typing import Any import voluptuous as vol @@ -108,8 +109,6 @@ class MediaPlayerGroup(MediaPlayerEntity): def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a Media Group entity.""" self._name = name - self._state: str | None = None - self._supported_features: int = 0 self._attr_unique_id = unique_id self._entities = entities @@ -207,16 +206,6 @@ class MediaPlayerGroup(MediaPlayerEntity): """Return the name of the entity.""" return self._name - @property - def state(self) -> str | None: - """Return the state of the media group.""" - return self._state - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - @property def extra_state_attributes(self) -> dict: """Return the state attributes for the media group.""" @@ -401,17 +390,19 @@ class MediaPlayerGroup(MediaPlayerEntity): ) if not valid_state: # Set as unknown if all members are unknown or unavailable - self._state = None + self._attr_state = None else: off_values = {MediaPlayerState.OFF, STATE_UNAVAILABLE, STATE_UNKNOWN} - if states.count(states[0]) == len(states): - self._state = states[0] + if states.count(single_state := states[0]) == len(states): + self._attr_state = None + with suppress(ValueError): + self._attr_state = MediaPlayerState(single_state) elif any(state for state in states if state not in off_values): - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF - supported_features = 0 + supported_features = MediaPlayerEntityFeature(0) if self._features[KEY_CLEAR_PLAYLIST]: supported_features |= MediaPlayerEntityFeature.CLEAR_PLAYLIST if self._features[KEY_TRACKS]: @@ -442,5 +433,5 @@ class MediaPlayerGroup(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_STEP ) - self._supported_features = supported_features + self._attr_supported_features = supported_features self.async_write_ha_state() diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index 5d63f29aff0..e1c4489e0e0 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -48,6 +48,12 @@ "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" }, "user": { + "description": "\u0413\u0440\u0443\u043f\u0438\u0442\u0435 \u0412\u0438 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0432\u0430\u0442 \u0434\u0430 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u0442\u0435 \u043d\u043e\u0432 \u043e\u0431\u0435\u043a\u0442, \u043a\u043e\u0439\u0442\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0432\u0430 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u043e\u0431\u0435\u043a\u0442\u0438 \u043e\u0442 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449\u0438 \u0442\u0438\u043f.", + "menu_options": { + "fan": "\u0413\u0440\u0443\u043f\u0430 \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440\u0438", + "light": "\u0413\u0440\u0443\u043f\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0438", + "media_player": "\u0413\u0440\u0443\u043f\u0430 \u043c\u0435\u0434\u0438\u0439\u043d\u0438 \u043f\u043b\u0435\u0439\u044a\u0440\u0438" + }, "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" } } diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index a2507082dda..9d0f16b4f61 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -66,7 +66,9 @@ "cover": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05d5\u05d9\u05dc\u05d5\u05e0\u05d5\u05ea", "fan": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05d0\u05d9\u05d5\u05d5\u05e8\u05d5\u05e8", "light": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05ea\u05d0\u05d5\u05e8\u05d4", - "media_player": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e0\u05d2\u05e0\u05d9 \u05de\u05d3\u05d9\u05d4" + "lock": "\u05e0\u05e2\u05d9\u05dc\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4", + "media_player": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e0\u05d2\u05e0\u05d9 \u05de\u05d3\u05d9\u05d4", + "switch": "\u05d4\u05d7\u05dc\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" } diff --git a/homeassistant/components/group/translations/sk.json b/homeassistant/components/group/translations/sk.json index 51759a9dc86..5bca7cea930 100644 --- a/homeassistant/components/group/translations/sk.json +++ b/homeassistant/components/group/translations/sk.json @@ -1,18 +1,121 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "V\u0161etky entity", + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov", + "name": "N\u00e1zov" + }, + "title": "Prida\u0165 skupinu" + }, + "cover": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov", + "name": "N\u00e1zov" + }, + "title": "Prida\u0165 skupinu" + }, + "fan": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov", + "name": "N\u00e1zov" + }, + "title": "Prida\u0165 skupinu" + }, + "light": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov", + "name": "N\u00e1zov" + }, + "title": "Prida\u0165 skupinu" + }, + "lock": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov", + "name": "N\u00e1zov" + }, + "title": "Prida\u0165 skupinu" + }, "media_player": { "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov", "name": "Meno" - } + }, + "title": "Prida\u0165 skupinu" + }, + "switch": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov", + "name": "N\u00e1zov" + }, + "title": "Prida\u0165 skupinu" + }, + "user": { + "menu_options": { + "binary_sensor": "Skupina sn\u00edma\u010d", + "fan": "Skupina ventil\u00e1tor", + "light": "Skupina osvetlenie", + "lock": "Skupina z\u00e1mok", + "media_player": "Skupina Media player", + "switch": "Skupina prep\u00edna\u010d" + }, + "title": "Prida\u0165 skupinu" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "V\u0161etky entity", + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov" + } + }, + "cover": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov" + } + }, + "fan": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov" + } + }, "light": { "data": { - "entities": "\u010clenovia" + "all": "V\u0161etky entity", + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov" + } + }, + "lock": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov" + } + }, + "media_player": { + "data": { + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov" + } + }, + "switch": { + "data": { + "all": "V\u0161etky entity", + "entities": "\u010clenovia", + "hide_members": "Skry\u0165 \u010dlenov" } } } diff --git a/homeassistant/components/group/translations/zh-Hant.json b/homeassistant/components/group/translations/zh-Hant.json index 023c76ebbba..327aa3a0def 100644 --- a/homeassistant/components/group/translations/zh-Hant.json +++ b/homeassistant/components/group/translations/zh-Hant.json @@ -8,7 +8,7 @@ "hide_members": "\u96b1\u85cf\u6210\u54e1", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", "title": "\u65b0\u589e\u7fa4\u7d44" }, "cover": { @@ -82,7 +82,7 @@ "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" }, "cover": { "data": { @@ -102,7 +102,7 @@ "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" }, "lock": { "data": { @@ -122,7 +122,7 @@ "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" } } }, diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index e3b63f7c8b3..3bb2317e0ef 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.2.4"], + "requirements": ["growattServer==1.3.0"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index d6b74b78475..d9ca800131f 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -80,7 +80,12 @@ async def async_setup_entry( config[CONF_URL] = url hass.config_entries.async_update_entry(config_entry, data=config) - api = growattServer.GrowattApi() + # Initialise the library with a random user id each time it is started, + # also extend the library's default identifier to include 'home-assistant' + api = growattServer.GrowattApi( + add_random_user_id=True, + agent_identifier=f"{growattServer.GrowattApi.agent_identifier} - home-assistant", + ) api.server_url = url devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) diff --git a/homeassistant/components/growatt_server/sensor_types/mix.py b/homeassistant/components/growatt_server/sensor_types/mix.py index 75d816fdf60..cfb47e81519 100644 --- a/homeassistant/components/growatt_server/sensor_types/mix.py +++ b/homeassistant/components/growatt_server/sensor_types/mix.py @@ -230,7 +230,6 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( key="mix_last_update", name="Last Data Update", api_key="lastdataupdate", - native_unit_of_measurement=None, device_class=SensorDeviceClass.TIMESTAMP, ), # Values from 'dashboard_data' API call diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py index 597ddd789cf..ba455747457 100644 --- a/homeassistant/components/growatt_server/sensor_types/tlx.py +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -20,6 +20,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( api_key="eacToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, precision=1, ), GrowattSensorEntityDescription( diff --git a/homeassistant/components/growatt_server/translations/hu.json b/homeassistant/components/growatt_server/translations/hu.json index 44d87bf753e..4e94af79900 100644 --- a/homeassistant/components/growatt_server/translations/hu.json +++ b/homeassistant/components/growatt_server/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_plants": "Ezen a sz\u00e1ml\u00e1n nem tal\u00e1ltak n\u00f6v\u00e9nyeket" + "no_plants": "Ebben a fi\u00f3kban nem tal\u00e1lhat\u00f3 egy er\u0151m\u0171 sem" }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" @@ -9,9 +9,9 @@ "step": { "plant": { "data": { - "plant_id": "N\u00f6v\u00e9ny" + "plant_id": "Er\u0151m\u0171" }, - "title": "V\u00e1lassza ki a n\u00f6v\u00e9ny\u00e9t" + "title": "V\u00e1lassza ki az er\u0151m\u0171vet" }, "user": { "data": { diff --git a/homeassistant/components/growatt_server/translations/sk.json b/homeassistant/components/growatt_server/translations/sk.json index d2e4793a68b..256fec69c1a 100644 --- a/homeassistant/components/growatt_server/translations/sk.json +++ b/homeassistant/components/growatt_server/translations/sk.json @@ -4,11 +4,21 @@ "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { + "plant": { + "data": { + "plant_id": "Rastlina" + } + }, "user": { "data": { - "name": "N\u00e1zov" - } + "name": "N\u00e1zov", + "password": "Heslo", + "url": "URL", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Zadajte inform\u00e1cie o svojom Growatte" } } - } + }, + "title": "Server Growatt" } \ No newline at end of file diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 2861dc4516b..cb6e6cee721 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from gsp import GstreamerPlayer +from gsp import STATE_IDLE, STATE_PAUSED, STATE_PLAYING, GstreamerPlayer import voluptuous as vol from homeassistant.components import media_source @@ -33,6 +33,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} ) +GSP_STATE_MAPPING = { + STATE_IDLE: MediaPlayerState.IDLE, + STATE_PAUSED: MediaPlayerState.PAUSED, + STATE_PLAYING: MediaPlayerState.PLAYING, +} + def setup_platform( hass: HomeAssistant, @@ -67,11 +73,11 @@ class GstreamerDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) - def __init__(self, player, name): + def __init__(self, player: GstreamerPlayer, name: str | None) -> None: """Initialize the Gstreamer device.""" self._player = player self._name = name or DOMAIN - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE self._volume = None self._duration = None self._uri = None @@ -81,7 +87,7 @@ class GstreamerDevice(MediaPlayerEntity): def update(self) -> None: """Update properties.""" - self._state = self._player.state + self._attr_state = GSP_STATE_MAPPING.get(self._player.state) self._volume = self._player.volume self._duration = self._player.duration self._uri = self._player.uri @@ -139,11 +145,6 @@ class GstreamerDevice(MediaPlayerEntity): """Return the volume level.""" return self._volume - @property - def state(self): - """Return the state of the player.""" - return self._state - @property def media_duration(self): """Duration of current playing media in seconds.""" diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 63fc66f685d..24999f98e16 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -44,15 +44,11 @@ from .util import GuardianDataUpdateCoordinator DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" -SERVICE_NAME_DISABLE_AP = "disable_ap" -SERVICE_NAME_ENABLE_AP = "enable_ap" SERVICE_NAME_PAIR_SENSOR = "pair_sensor" SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" SERVICE_NAME_UPGRADE_FIRMWARE = "upgrade_firmware" SERVICES = ( - SERVICE_NAME_DISABLE_AP, - SERVICE_NAME_ENABLE_AP, SERVICE_NAME_PAIR_SENSOR, SERVICE_NAME_UNPAIR_SENSOR, SERVICE_NAME_UPGRADE_FIRMWARE, @@ -231,30 +227,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @call_with_data - async def async_disable_ap(call: ServiceCall, data: GuardianData) -> None: - """Disable the onboard AP.""" - async_log_deprecated_service_call( - hass, - call, - "switch.turn_off", - f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap", - "2022.12.0", - ) - await data.client.wifi.disable_ap() - - @call_with_data - async def async_enable_ap(call: ServiceCall, data: GuardianData) -> None: - """Enable the onboard AP.""" - async_log_deprecated_service_call( - hass, - call, - "switch.turn_on", - f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap", - "2022.12.0", - ) - await data.client.wifi.enable_ap() - @call_with_data async def async_pair_sensor(call: ServiceCall, data: GuardianData) -> None: """Add a new paired sensor.""" @@ -279,8 +251,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service_name, schema, method in ( - (SERVICE_NAME_DISABLE_AP, SERVICE_BASE_SCHEMA, async_disable_ap), - (SERVICE_NAME_ENABLE_AP, SERVICE_BASE_SCHEMA, async_enable_ap), ( SERVICE_NAME_PAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 6425ecd46a6..a2f2a7a593e 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -23,7 +23,6 @@ from . import ( ) from .const import ( API_SYSTEM_ONBOARD_SENSOR_STATUS, - API_WIFI_STATUS, CONF_UID, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -36,7 +35,6 @@ from .util import ( ATTR_CONNECTED_CLIENTS = "connected_clients" -SENSOR_KIND_AP_INFO = "ap_enabled" SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSOR_KIND_MOVED = "moved" @@ -69,13 +67,6 @@ VALVE_CONTROLLER_DESCRIPTIONS = ( device_class=BinarySensorDeviceClass.MOISTURE, api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS, ), - ValveControllerBinarySensorDescription( - key=SENSOR_KIND_AP_INFO, - name="Onboard AP enabled", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - entity_category=EntityCategory.DIAGNOSTIC, - api_category=API_WIFI_STATUS, - ), ) @@ -95,7 +86,7 @@ async def async_setup_entry( f"{uid}_ap_enabled", f"switch.guardian_valve_controller_{uid}_onboard_ap", "2022.12.0", - remove_old_entity=False, + remove_old_entity=True, ), ), ) @@ -183,10 +174,5 @@ class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" - if self.entity_description.key == SENSOR_KIND_AP_INFO: - self._attr_is_on = self.coordinator.data["station_connected"] - self._attr_extra_state_attributes[ - ATTR_CONNECTED_CLIENTS - ] = self.coordinator.data.get("ap_clients") - elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: + if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: self._attr_is_on = self.coordinator.data["wet"] diff --git a/homeassistant/components/guardian/translations/bg.json b/homeassistant/components/guardian/translations/bg.json index d48caec927f..c8098f9355a 100644 --- a/homeassistant/components/guardian/translations/bg.json +++ b/homeassistant/components/guardian/translations/bg.json @@ -23,17 +23,6 @@ } }, "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0441\u0438\u0447\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432\u0435, \u043a\u043e\u0438\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 \u0442\u043e\u0437\u0438 \u043e\u0431\u0435\u043a\u0442, \u0442\u0430\u043a\u0430 \u0447\u0435 \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0433\u043e \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 `{replacement_entity_id}`.", - "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" - } - } - }, - "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index dee6614924d..709edc2476f 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -29,17 +29,6 @@ } }, "title": "El servei {deprecated_service} ser\u00e0 eliminat" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat perqu\u00e8 passin a utilitzar l'entitat `{replacement_entity_id}`.", - "title": "L'entitat {old_entity_id} s'eliminar\u00e0" - } - } - }, - "title": "L'entitat {old_entity_id} s'eliminar\u00e0" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 9d5c0c0265c..ac9c71d14e1 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -29,17 +29,6 @@ } }, "title": "Der Dienst {deprecated_service} wird entfernt" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t verwenden, um stattdessen `{replacement_entity_id}` zu verwenden.", - "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" - } - } - }, - "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 0857e0284a4..2a4963c8649 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -29,17 +29,6 @@ } }, "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03bf `{replacement_entity_id}`.", - "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" - } - } - }, - "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index ac87ae36506..1aaf8b888c8 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -29,17 +29,6 @@ } }, "title": "The {deprecated_service} service will be removed" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", - "title": "The {old_entity_id} entity will be removed" - } - } - }, - "title": "The {old_entity_id} entity will be removed" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 16233859836..e1e189a650f 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -29,17 +29,6 @@ } }, "title": "Se va a eliminar el servicio {deprecated_service}" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad para usar `{replacement_entity_id}`.", - "title": "Se eliminar\u00e1 la entidad {old_entity_id}" - } - } - }, - "title": "Se eliminar\u00e1 la entidad {old_entity_id}" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 95c98315435..2af9912946d 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -29,17 +29,6 @@ } }, "title": "Teenus {deprecated_service} eemaldatakse" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Uuenda k\u00f5iki automaatikaid v\u00f5i skripte, mis kasutavad seda olemit, et kasutada selle asemel `{replacement_entity_id}}.", - "title": "Olem {old_entity_id} eemaldatakse" - } - } - }, - "title": "Olem {old_entity_id} eemaldatakse" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json index 253d28ec770..eead1a91a22 100644 --- a/homeassistant/components/guardian/translations/fr.json +++ b/homeassistant/components/guardian/translations/fr.json @@ -29,17 +29,6 @@ } }, "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Modifiez tout script ou automatisation utilisant cette entit\u00e9 afin qu'ils utilisent `{replacement_entity_id}` \u00e0 la place.", - "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" - } - } - }, - "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 20b5d00e2f1..c9ad858f34f 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -29,17 +29,6 @@ } }, "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Friss\u00edtse az ezt az entit\u00e1st haszn\u00e1l\u00f3 automatiz\u00e1l\u00e1sokat vagy szkripteket, hogy helyette a k\u00f6vetkez\u0151t haszn\u00e1ja: `{replacement_entity_id}`", - "title": "{old_entity_id} entit\u00e1s el lesz t\u00e1vol\u00edtva" - } - } - }, - "title": "{old_entity_id} entit\u00e1s el lesz t\u00e1vol\u00edtva" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index e62f48eae8f..6558dfa262d 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -29,17 +29,6 @@ } }, "title": "Layanan {deprecated_service} akan dihapus" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Perbarui setiap otomasi atau skrip yang menggunakan entitas ini untuk menggunakan `{replacement_entity_id}`.", - "title": "Entitas {old_entity_id} akan dihapus" - } - } - }, - "title": "Entitas {old_entity_id} akan dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index 29c1d90ed87..7b5062222d2 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -29,17 +29,6 @@ } }, "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aggiorna tutte le automazioni o gli script che utilizzano questa entit\u00e0 in modo che utilizzino invece `{replacement_entity_id}`.", - "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" - } - } - }, - "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json index 0fadd4dbf51..39a397375e2 100644 --- a/homeassistant/components/guardian/translations/nl.json +++ b/homeassistant/components/guardian/translations/nl.json @@ -28,16 +28,6 @@ } }, "title": "De {deprecated_service}-service wordt verwijderd" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "title": "De {old_entity_id}-entiteit wordt verwijderd" - } - } - }, - "title": "De {old_entity_id}-entiteit wordt verwijderd" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index b550bf9c43d..425b973aa5d 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -29,17 +29,6 @@ } }, "title": "{deprecated_service} -tjenesten vil bli fjernet" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten til i stedet \u00e5 bruke ` {replacement_entity_id} `.", - "title": "{old_entity_id} vil bli fjernet" - } - } - }, - "title": "{old_entity_id} vil bli fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index f1cb2893050..4fb2f523807 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -29,17 +29,6 @@ } }, "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej encji, aby zamiast tego u\u017cywa\u0142y `{replacement_entity_id}`.", - "title": "Encja {old_entity_id} zostanie usuni\u0119ta" - } - } - }, - "title": "Encja {old_entity_id} zostanie usuni\u0119ta" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json index 53e3c724b7c..b01f925744e 100644 --- a/homeassistant/components/guardian/translations/pt-BR.json +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -29,17 +29,6 @@ } }, "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam essa entidade para usar `{replacement_entity_id}`.", - "title": "A entidade {old_entity_id} ser\u00e1 removida" - } - } - }, - "title": "A entidade {old_entity_id} ser\u00e1 removida" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index 6cfc857f0de..7e64fe528ff 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -29,17 +29,6 @@ } }, "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 `{replacement_entity_id}`.", - "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" - } - } - }, - "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sk.json b/homeassistant/components/guardian/translations/sk.json index b41d6edbd4b..1e35b169d86 100644 --- a/homeassistant/components/guardian/translations/sk.json +++ b/homeassistant/components/guardian/translations/sk.json @@ -1,14 +1,30 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165" }, "step": { "user": { "data": { + "ip_address": "IP adresa", "port": "Port" } } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } + } + }, + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sv.json b/homeassistant/components/guardian/translations/sv.json index 0912dd4094b..af41cc85efe 100644 --- a/homeassistant/components/guardian/translations/sv.json +++ b/homeassistant/components/guardian/translations/sv.json @@ -29,17 +29,6 @@ } }, "title": "Tj\u00e4nsten {deprecated_service} tas bort" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Uppdatera alla automatiseringar eller skript som anv\u00e4nder denna enhet f\u00f6r att ist\u00e4llet anv\u00e4nda ` {replacement_entity_id} `.", - "title": "{old_entity_id} kommer att tas bort" - } - } - }, - "title": "{old_entity_id} kommer att tas bort" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index c7e557a38f2..dea0e3fa9ec 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -29,17 +29,6 @@ } }, "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Bunun yerine ` {replacement_entity_id} ` kullanmak i\u00e7in bu varl\u0131\u011f\u0131 kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 g\u00fcncelleyin.", - "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" - } - } - }, - "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index bd30a848b35..54649e2bf31 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -29,17 +29,6 @@ } }, "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\uff0c\u4ee5\u53d6\u4ee3 `{replacement_entity_id}`\u3002", - "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" - } - } - }, - "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/sk.json b/homeassistant/components/habitica/translations/sk.json index bcfc3880d99..25aa532742a 100644 --- a/homeassistant/components/habitica/translations/sk.json +++ b/homeassistant/components/habitica/translations/sk.json @@ -1,12 +1,15 @@ { "config": { "error": { - "invalid_credentials": "Neplatn\u00e9 overenie" + "invalid_credentials": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" + "api_key": "API k\u013e\u00fa\u010d", + "name": "Prep\u00edsa\u0165 pou\u017e\u00edvate\u013esk\u00e9 meno Habitica. Bude sa pou\u017e\u00edva\u0165 na servisn\u00e9 hovory", + "url": "URL" } } } diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py deleted file mode 100644 index 6b0e10705c0..00000000000 --- a/homeassistant/components/hangouts/__init__.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Support for Hangouts.""" -import logging - -from hangups.auth import GoogleAuthError -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components.conversation.util import create_matcher -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers import dispatcher, intent -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType - -# We need an import from .config_flow, without it .config_flow is never loaded. -from .config_flow import HangoutsFlowHandler # noqa: F401 -from .const import ( - CONF_BOT, - CONF_DEFAULT_CONVERSATIONS, - CONF_ERROR_SUPPRESSED_CONVERSATIONS, - CONF_INTENTS, - CONF_MATCHERS, - CONF_REFRESH_TOKEN, - CONF_SENTENCES, - DOMAIN, - EVENT_HANGOUTS_CONNECTED, - EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, - INTENT_HELP, - INTENT_SCHEMA, - MESSAGE_SCHEMA, - SERVICE_RECONNECT, - SERVICE_SEND_MESSAGE, - SERVICE_UPDATE, - TARGETS_SCHEMA, -) -from .hangouts_bot import HangoutsBot -from .intents import HelpIntent - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_INTENTS, default={}): vol.Schema( - {cv.string: INTENT_SCHEMA} - ), - vol.Optional(CONF_DEFAULT_CONVERSATIONS, default=[]): [TARGETS_SCHEMA], - vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]): [ - TARGETS_SCHEMA - ], - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Hangouts bot component.""" - if (conf := config.get(DOMAIN)) is None: - hass.data[DOMAIN] = { - CONF_INTENTS: {}, - CONF_DEFAULT_CONVERSATIONS: [], - CONF_ERROR_SUPPRESSED_CONVERSATIONS: [], - } - return True - - hass.data[DOMAIN] = { - CONF_INTENTS: conf[CONF_INTENTS], - CONF_DEFAULT_CONVERSATIONS: conf[CONF_DEFAULT_CONVERSATIONS], - CONF_ERROR_SUPPRESSED_CONVERSATIONS: conf[CONF_ERROR_SUPPRESSED_CONVERSATIONS], - } - - if ( - hass.data[DOMAIN][CONF_INTENTS] - and INTENT_HELP not in hass.data[DOMAIN][CONF_INTENTS] - ): - hass.data[DOMAIN][CONF_INTENTS][INTENT_HELP] = {CONF_SENTENCES: ["HELP"]} - - for data in hass.data[DOMAIN][CONF_INTENTS].values(): - matchers = [] - for sentence in data[CONF_SENTENCES]: - matchers.append(create_matcher(sentence)) - - data[CONF_MATCHERS] = matchers - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: - """Set up a config entry.""" - try: - bot = HangoutsBot( - hass, - config.data.get(CONF_REFRESH_TOKEN), - hass.data[DOMAIN][CONF_INTENTS], - hass.data[DOMAIN][CONF_DEFAULT_CONVERSATIONS], - hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS], - ) - hass.data[DOMAIN][CONF_BOT] = bot - except GoogleAuthError as exception: - _LOGGER.error("Hangouts failed to log in: %s", str(exception)) - return False - - dispatcher.async_dispatcher_connect( - hass, EVENT_HANGOUTS_CONNECTED, bot.async_handle_update_users_and_conversations - ) - - dispatcher.async_dispatcher_connect( - hass, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, bot.async_resolve_conversations - ) - - dispatcher.async_dispatcher_connect( - hass, - EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, - bot.async_update_conversation_commands, - ) - - config.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) - ) - - await bot.async_connect() - - hass.services.async_register( - DOMAIN, - SERVICE_SEND_MESSAGE, - bot.async_handle_send_message, - schema=MESSAGE_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_UPDATE, - bot.async_handle_update_users_and_conversations, - schema=vol.Schema({}), - ) - - hass.services.async_register( - DOMAIN, SERVICE_RECONNECT, bot.async_handle_reconnect, schema=vol.Schema({}) - ) - - intent.async_register(hass, HelpIntent(hass)) - - return True - - -async def async_unload_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: - """Unload a config entry.""" - bot = hass.data[DOMAIN].pop(CONF_BOT) - await bot.async_disconnect() - return True diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py deleted file mode 100644 index 598c7fbd9cb..00000000000 --- a/homeassistant/components/hangouts/config_flow.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Config flow to configure Google Hangouts.""" -import functools - -from hangups import get_auth -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD - -from .const import CONF_2FA, CONF_AUTH_CODE, CONF_REFRESH_TOKEN, DOMAIN -from .hangups_utils import ( - Google2FAError, - GoogleAuthError, - HangoutsCredentials, - HangoutsRefreshToken, -) - - -class HangoutsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Config flow Google Hangouts.""" - - VERSION = 1 - - def __init__(self): - """Initialize Google Hangouts config flow.""" - self._credentials = None - self._refresh_token = None - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - errors = {} - - self._async_abort_entries_match() - - if user_input is not None: - user_email = user_input[CONF_EMAIL] - user_password = user_input[CONF_PASSWORD] - user_auth_code = user_input.get(CONF_AUTH_CODE) - manual_login = user_auth_code is not None - - user_pin = None - self._credentials = HangoutsCredentials( - user_email, user_password, user_pin, user_auth_code - ) - self._refresh_token = HangoutsRefreshToken(None) - try: - await self.hass.async_add_executor_job( - functools.partial( - get_auth, - self._credentials, - self._refresh_token, - manual_login=manual_login, - ) - ) - - return await self.async_step_final() - except GoogleAuthError as err: - if isinstance(err, Google2FAError): - return await self.async_step_2fa() - msg = str(err) - if msg == "Unknown verification code input": - errors["base"] = "invalid_2fa_method" - else: - errors["base"] = "invalid_login" - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_AUTH_CODE): str, - } - ), - errors=errors, - ) - - async def async_step_2fa(self, user_input=None): - """Handle the 2fa step, if needed.""" - errors = {} - - if user_input is not None: - self._credentials.set_verification_code(user_input[CONF_2FA]) - try: - await self.hass.async_add_executor_job( - get_auth, self._credentials, self._refresh_token - ) - - return await self.async_step_final() - except GoogleAuthError: - errors["base"] = "invalid_2fa" - - return self.async_show_form( - step_id=CONF_2FA, - data_schema=vol.Schema({vol.Required(CONF_2FA): str}), - errors=errors, - ) - - async def async_step_final(self): - """Handle the final step, create the config entry.""" - return self.async_create_entry( - title=self._credentials.get_email(), - data={ - CONF_EMAIL: self._credentials.get_email(), - CONF_REFRESH_TOKEN: self._refresh_token.get(), - }, - ) - - async def async_step_import(self, _): - """Handle a flow import.""" - return await self.async_step_user() diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py deleted file mode 100644 index 3a78e9bbe80..00000000000 --- a/homeassistant/components/hangouts/const.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Constants for Google Hangouts Component.""" -import voluptuous as vol - -from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET -import homeassistant.helpers.config_validation as cv - -DOMAIN = "hangouts" - -CONF_2FA = "2fa" -CONF_AUTH_CODE = "authorization_code" -CONF_REFRESH_TOKEN = "refresh_token" -CONF_BOT = "bot" - -CONF_CONVERSATIONS = "conversations" -CONF_DEFAULT_CONVERSATIONS = "default_conversations" -CONF_ERROR_SUPPRESSED_CONVERSATIONS = "error_suppressed_conversations" - -CONF_INTENTS = "intents" -CONF_INTENT_TYPE = "intent_type" -CONF_SENTENCES = "sentences" -CONF_MATCHERS = "matchers" - -INTENT_HELP = "HangoutsHelp" - -EVENT_HANGOUTS_CONNECTED = "hangouts_connected" -EVENT_HANGOUTS_DISCONNECTED = "hangouts_disconnected" -EVENT_HANGOUTS_USERS_CHANGED = "hangouts_users_changed" -EVENT_HANGOUTS_CONVERSATIONS_CHANGED = "hangouts_conversations_changed" -EVENT_HANGOUTS_CONVERSATIONS_RESOLVED = "hangouts_conversations_resolved" -EVENT_HANGOUTS_MESSAGE_RECEIVED = "hangouts_message_received" - -CONF_CONVERSATION_ID = "id" -CONF_CONVERSATION_NAME = "name" - -SERVICE_SEND_MESSAGE = "send_message" -SERVICE_UPDATE = "update" -SERVICE_RECONNECT = "reconnect" - - -TARGETS_SCHEMA = vol.All( - vol.Schema( - { - vol.Exclusive(CONF_CONVERSATION_ID, "id or name"): cv.string, - vol.Exclusive(CONF_CONVERSATION_NAME, "id or name"): cv.string, - } - ), - cv.has_at_least_one_key(CONF_CONVERSATION_ID, CONF_CONVERSATION_NAME), -) -MESSAGE_SEGMENT_SCHEMA = vol.Schema( - { - vol.Required("text"): cv.string, - vol.Optional("is_bold"): cv.boolean, - vol.Optional("is_italic"): cv.boolean, - vol.Optional("is_strikethrough"): cv.boolean, - vol.Optional("is_underline"): cv.boolean, - vol.Optional("parse_str"): cv.boolean, - vol.Optional("link_target"): cv.string, - } -) -MESSAGE_DATA_SCHEMA = vol.Schema( - {vol.Optional("image_file"): cv.string, vol.Optional("image_url"): cv.string} -) - -MESSAGE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TARGET): [TARGETS_SCHEMA], - vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA], - vol.Optional(ATTR_DATA): MESSAGE_DATA_SCHEMA, - } -) - -INTENT_SCHEMA = vol.All( - # Basic Schema - vol.Schema( - { - vol.Required(CONF_SENTENCES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_CONVERSATIONS): [TARGETS_SCHEMA], - } - ) -) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py deleted file mode 100644 index c3c363ef55e..00000000000 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ /dev/null @@ -1,361 +0,0 @@ -"""The Hangouts Bot.""" -from __future__ import annotations - -import asyncio -from contextlib import suppress -from http import HTTPStatus -import io -import logging - -import aiohttp -import hangups -from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 - -from homeassistant.core import ServiceCall, callback -from homeassistant.helpers import dispatcher, intent -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import ( - ATTR_DATA, - ATTR_MESSAGE, - ATTR_TARGET, - CONF_CONVERSATION_ID, - CONF_CONVERSATION_NAME, - CONF_CONVERSATIONS, - CONF_MATCHERS, - DOMAIN, - EVENT_HANGOUTS_CONNECTED, - EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, - EVENT_HANGOUTS_DISCONNECTED, - EVENT_HANGOUTS_MESSAGE_RECEIVED, - INTENT_HELP, -) -from .hangups_utils import HangoutsCredentials, HangoutsRefreshToken - -_LOGGER = logging.getLogger(__name__) - - -class HangoutsBot: - """The Hangouts Bot.""" - - def __init__( - self, hass, refresh_token, intents, default_convs, error_suppressed_convs - ): - """Set up the client.""" - self.hass = hass - self._connected = False - - self._refresh_token = refresh_token - - self._intents = intents - self._conversation_intents = None - - self._client = None - self._user_list = None - self._conversation_list = None - self._default_convs = default_convs - self._default_conv_ids = None - self._error_suppressed_convs = error_suppressed_convs - self._error_suppressed_conv_ids = None - - dispatcher.async_dispatcher_connect( - self.hass, - EVENT_HANGOUTS_MESSAGE_RECEIVED, - self._async_handle_conversation_message, - ) - - def _resolve_conversation_id(self, obj): - if CONF_CONVERSATION_ID in obj: - return obj[CONF_CONVERSATION_ID] - if CONF_CONVERSATION_NAME in obj: - conv = self._resolve_conversation_name(obj[CONF_CONVERSATION_NAME]) - if conv is not None: - return conv.id_ - return None - - def _resolve_conversation_name(self, name): - for conv in self._conversation_list.get_all(): - if conv.name == name: - return conv - return None - - @callback - def async_update_conversation_commands(self): - """Refresh the commands for every conversation.""" - self._conversation_intents = {} - - for intent_type, data in self._intents.items(): - if data.get(CONF_CONVERSATIONS): - conversations = [] - for conversation in data.get(CONF_CONVERSATIONS): - conv_id = self._resolve_conversation_id(conversation) - if conv_id is not None: - conversations.append(conv_id) - data[f"_{CONF_CONVERSATIONS}"] = conversations - elif self._default_conv_ids: - data[f"_{CONF_CONVERSATIONS}"] = self._default_conv_ids - else: - data[f"_{CONF_CONVERSATIONS}"] = [ - conv.id_ for conv in self._conversation_list.get_all() - ] - - for conv_id in data[f"_{CONF_CONVERSATIONS}"]: - if conv_id not in self._conversation_intents: - self._conversation_intents[conv_id] = {} - - self._conversation_intents[conv_id][intent_type] = data - - with suppress(ValueError): - self._conversation_list.on_event.remove_observer( - self._async_handle_conversation_event - ) - self._conversation_list.on_event.add_observer( - self._async_handle_conversation_event - ) - - @callback - def async_resolve_conversations(self, _): - """Resolve the list of default and error suppressed conversations.""" - self._default_conv_ids = [] - self._error_suppressed_conv_ids = [] - - for conversation in self._default_convs: - conv_id = self._resolve_conversation_id(conversation) - if conv_id is not None: - self._default_conv_ids.append(conv_id) - - for conversation in self._error_suppressed_convs: - conv_id = self._resolve_conversation_id(conversation) - if conv_id is not None: - self._error_suppressed_conv_ids.append(conv_id) - dispatcher.async_dispatcher_send( - self.hass, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED - ) - - async def _async_handle_conversation_event(self, event): - if isinstance(event, ChatMessageEvent): - dispatcher.async_dispatcher_send( - self.hass, - EVENT_HANGOUTS_MESSAGE_RECEIVED, - event.conversation_id, - event.user_id, - event, - ) - - async def _async_handle_conversation_message(self, conv_id, user_id, event): - """Handle a message sent to a conversation.""" - user = self._user_list.get_user(user_id) - if user.is_self: - return - message = event.text - - _LOGGER.debug("Handling message '%s' from %s", message, user.full_name) - - intents = self._conversation_intents.get(conv_id) - if intents is not None: - is_error = False - try: - intent_result = await self._async_process(intents, message, conv_id) - except (intent.UnknownIntent, intent.IntentHandleError) as err: - is_error = True - intent_result = intent.IntentResponse() - intent_result.async_set_speech(str(err)) - - if intent_result is None: - is_error = True - intent_result = intent.IntentResponse() - intent_result.async_set_speech("Sorry, I didn't understand that") - - message = ( - intent_result.as_dict().get("speech", {}).get("plain", {}).get("speech") - ) - - if (message is not None) and not ( - is_error and conv_id in self._error_suppressed_conv_ids - ): - await self._async_send_message( - [{"text": message, "parse_str": True}], - [{CONF_CONVERSATION_ID: conv_id}], - None, - ) - - async def _async_process(self, intents, text, conv_id): - """Detect a matching intent.""" - for intent_type, data in intents.items(): - for matcher in data.get(CONF_MATCHERS, []): - if not (match := matcher.match(text)): - continue - if intent_type == INTENT_HELP: - return await intent.async_handle( - self.hass, - DOMAIN, - intent_type, - {"conv_id": {"value": conv_id}}, - text, - ) - - return await intent.async_handle( - self.hass, - DOMAIN, - intent_type, - {"conv_id": {"value": conv_id}} - | { - key: {"value": value} - for key, value in match.groupdict().items() - }, - text, - ) - - async def async_connect(self): - """Login to the Google Hangouts.""" - session = await self.hass.async_add_executor_job( - get_auth, - HangoutsCredentials(None, None, None), - HangoutsRefreshToken(self._refresh_token), - ) - - self._client = Client(session) - self._client.on_connect.add_observer(self._on_connect) - self._client.on_disconnect.add_observer(self._on_disconnect) - - self.hass.loop.create_task(self._client.connect()) - - def _on_connect(self): - _LOGGER.debug("Connected!") - self._connected = True - dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_CONNECTED) - - async def _on_disconnect(self): - """Handle disconnecting.""" - if self._connected: - _LOGGER.debug("Connection lost! Reconnect") - await self.async_connect() - else: - dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_DISCONNECTED) - - async def async_disconnect(self): - """Disconnect the client if it is connected.""" - if self._connected: - self._connected = False - await self._client.disconnect() - - async def async_handle_hass_stop(self, _): - """Run once when Home Assistant stops.""" - await self.async_disconnect() - - async def _async_send_message(self, message, targets, data): - conversations = [] - for target in targets: - conversation = None - if CONF_CONVERSATION_ID in target: - conversation = self._conversation_list.get(target[CONF_CONVERSATION_ID]) - elif CONF_CONVERSATION_NAME in target: - conversation = self._resolve_conversation_name( - target[CONF_CONVERSATION_NAME] - ) - if conversation is not None: - conversations.append(conversation) - - if not conversations: - return False - - messages = [] - for segment in message: - if messages: - messages.append( - ChatMessageSegment( - "", segment_type=hangouts_pb2.SEGMENT_TYPE_LINE_BREAK - ) - ) - if "parse_str" in segment and segment["parse_str"]: - messages.extend(ChatMessageSegment.from_str(segment["text"])) - else: - if "parse_str" in segment: - del segment["parse_str"] - messages.append(ChatMessageSegment(**segment)) - - image_file = None - if data: - if data.get("image_url"): - uri = data.get("image_url") - try: - websession = async_get_clientsession(self.hass) - async with websession.get(uri, timeout=5) as response: - if response.status != HTTPStatus.OK: - _LOGGER.error( - "Fetch image failed, %s, %s", response.status, response - ) - image_file = None - else: - image_data = await response.read() - image_file = io.BytesIO(image_data) - image_file.name = "image.png" - except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error("Failed to fetch image, %s", type(error)) - image_file = None - elif data.get("image_file"): - uri = data.get("image_file") - if self.hass.config.is_allowed_path(uri): - try: - # pylint: disable=consider-using-with - image_file = open(uri, "rb") - except OSError as error: - _LOGGER.error( - "Image file I/O error(%s): %s", error.errno, error.strerror - ) - else: - _LOGGER.error('Path "%s" not allowed', uri) - - if not messages: - return False - for conv in conversations: - await conv.send_message(messages, image_file) - - async def _async_list_conversations(self): - ( - self._user_list, - self._conversation_list, - ) = await hangups.build_user_conversation_list(self._client) - conversations = {} - for i, conv in enumerate(self._conversation_list.get_all()): - users_in_conversation = [] - for user in conv.users: - users_in_conversation.append(user.full_name) - conversations[str(i)] = { - CONF_CONVERSATION_ID: str(conv.id_), - CONF_CONVERSATION_NAME: conv.name, - "users": users_in_conversation, - } - - self.hass.states.async_set( - f"{DOMAIN}.conversations", - len(self._conversation_list.get_all()), - attributes=conversations, - ) - dispatcher.async_dispatcher_send( - self.hass, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, conversations - ) - - async def async_handle_send_message(self, service: ServiceCall) -> None: - """Handle the send_message service.""" - await self._async_send_message( - service.data[ATTR_MESSAGE], - service.data[ATTR_TARGET], - service.data.get(ATTR_DATA, {}), - ) - - async def async_handle_update_users_and_conversations( - self, service: ServiceCall | None = None - ) -> None: - """Handle the update_users_and_conversations service.""" - await self._async_list_conversations() - - async def async_handle_reconnect(self, service: ServiceCall | None = None) -> None: - """Handle the reconnect service.""" - await self.async_disconnect() - await self.async_connect() - - def get_intents(self, conv_id): - """Return the intents for a specific conversation.""" - return self._conversation_intents.get(conv_id) diff --git a/homeassistant/components/hangouts/hangups_utils.py b/homeassistant/components/hangouts/hangups_utils.py deleted file mode 100644 index d2556ac15a0..00000000000 --- a/homeassistant/components/hangouts/hangups_utils.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Utils needed for Google Hangouts.""" - -from hangups import CredentialsPrompt, GoogleAuthError, RefreshTokenCache - - -class Google2FAError(GoogleAuthError): - """A Google authentication request failed.""" - - -class HangoutsCredentials(CredentialsPrompt): - """Google account credentials. - - This implementation gets the user data as params. - """ - - def __init__(self, email, password, pin=None, auth_code=None): - """Google account credentials. - - :param email: Google account email address. - :param password: Google account password. - :param pin: Google account verification code. - """ - self._email = email - self._password = password - self._pin = pin - self._auth_code = auth_code - - def get_email(self): - """Return email. - - :return: Google account email address. - """ - return self._email - - def get_password(self): - """Return password. - - :return: Google account password. - """ - return self._password - - def get_verification_code(self): - """Return the verification code. - - :return: Google account verification code. - """ - if self._pin is None: - raise Google2FAError() - return self._pin - - def set_verification_code(self, pin): - """Set the verification code. - - :param pin: Google account verification code. - """ - self._pin = pin - - def get_authorization_code(self): - """Return the oauth authorization code. - - :return: Google oauth code. - """ - return self._auth_code - - def set_authorization_code(self, code): - """Set the google oauth authorization code. - - :param code: Oauth code returned after authentication with google. - """ - self._auth_code = code - - -class HangoutsRefreshToken(RefreshTokenCache): - """Memory-based cache for refresh token.""" - - def __init__(self, token): - """Memory-based cache for refresh token. - - :param token: Initial refresh token. - """ - super().__init__("") - self._token = token - - def get(self): - """Get cached refresh token. - - :return: Cached refresh token. - """ - return self._token - - def set(self, refresh_token): - """Cache a refresh token. - - :param refresh_token: Refresh token to cache. - """ - self._token = refresh_token diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py deleted file mode 100644 index 5e4c6ff206b..00000000000 --- a/homeassistant/components/hangouts/intents.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Intents for the Hangouts component.""" -from homeassistant.helpers import intent -import homeassistant.helpers.config_validation as cv - -from .const import CONF_BOT, DOMAIN, INTENT_HELP - - -class HelpIntent(intent.IntentHandler): - """Handle Help intents.""" - - intent_type = INTENT_HELP - slot_schema = {"conv_id": cv.string} - - def __init__(self, hass): - """Set up the intent.""" - self.hass = hass - - async def async_handle(self, intent_obj): - """Handle the intent.""" - slots = self.async_validate_slots(intent_obj.slots) - conv_id = slots["conv_id"]["value"] - - intents = self.hass.data[DOMAIN][CONF_BOT].get_intents(conv_id) - response = intent_obj.create_response() - help_text = "I understand the following sentences:" - for intent_data in intents.values(): - for sentence in intent_data["sentences"]: - help_text += f"\n'{sentence}'" - response.async_set_speech(help_text) - - return response diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json deleted file mode 100644 index b8b1004bb78..00000000000 --- a/homeassistant/components/hangouts/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "hangouts", - "name": "Google Chat", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": ["hangups==0.4.18"], - "codeowners": [], - "iot_class": "cloud_push", - "loggers": ["hangups", "urwid"] -} diff --git a/homeassistant/components/hangouts/notify.py b/homeassistant/components/hangouts/notify.py deleted file mode 100644 index 77dcec8111a..00000000000 --- a/homeassistant/components/hangouts/notify.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Support for Hangouts notifications.""" -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_MESSAGE, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) - -from .const import ( - CONF_DEFAULT_CONVERSATIONS, - DOMAIN, - SERVICE_SEND_MESSAGE, - TARGETS_SCHEMA, -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_DEFAULT_CONVERSATIONS): [TARGETS_SCHEMA]} -) - - -def get_service(hass, config, discovery_info=None): - """Get the Hangouts notification service.""" - return HangoutsNotificationService(config.get(CONF_DEFAULT_CONVERSATIONS)) - - -class HangoutsNotificationService(BaseNotificationService): - """Send Notifications to Hangouts conversations.""" - - def __init__(self, default_conversations): - """Set up the notification service.""" - self._default_conversations = default_conversations - - def send_message(self, message="", **kwargs): - """Send the message to the Google Hangouts server.""" - target_conversations = None - if ATTR_TARGET in kwargs: - target_conversations = [] - for target in kwargs.get(ATTR_TARGET): - target_conversations.append({"id": target}) - else: - target_conversations = self._default_conversations - - messages = [] - if "title" in kwargs: - messages.append({"text": kwargs["title"], "is_bold": True}) - - messages.append({"text": message, "parse_str": True}) - service_data = {ATTR_TARGET: target_conversations, ATTR_MESSAGE: messages} - if kwargs[ATTR_DATA]: - service_data[ATTR_DATA] = kwargs[ATTR_DATA] - - return self.hass.services.call( - DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data - ) diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml deleted file mode 100644 index 041c21b5c25..00000000000 --- a/homeassistant/components/hangouts/services.yaml +++ /dev/null @@ -1,32 +0,0 @@ -update: - name: Update - description: Updates the list of conversations. - -send_message: - name: Send message - description: Send a notification to a specific target. - fields: - target: - name: Target - description: List of targets with id or name. - required: true - example: '[{"id": "UgxrXzVrARmjx_C6AZx4AaABAagBo-6UCw"}, {"name": "Test Conversation"}]' - selector: - object: - message: - name: Message - description: List of message segments, only the "text" field is required in every segment. - required: true - example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}]' - selector: - object: - data: - name: Data - description: Other options ['image_file' / 'image_url'] - example: '{ "image_file": "file" }' - selector: - object: - -reconnect: - name: Reconnect - description: Reconnect the bot. diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json deleted file mode 100644 index fcc2da456bb..00000000000 --- a/homeassistant/components/hangouts/strings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "error": { - "invalid_login": "Invalid Login, please try again.", - "invalid_2fa": "Invalid 2 Factor Authentication, please try again.", - "invalid_2fa_method": "Invalid 2FA Method (verify on Phone)." - }, - "step": { - "user": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "authorization_code": "Authorization Code (required for manual authentication)" - }, - "title": "Google Chat Login" - }, - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "title": "2-Factor-Authentication" - } - } - } -} diff --git a/homeassistant/components/hangouts/translations/bg.json b/homeassistant/components/hangouts/translations/bg.json deleted file mode 100644 index 8d8dae90ce0..00000000000 --- a/homeassistant/components/hangouts/translations/bg.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "unknown": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430." - }, - "error": { - "invalid_2fa": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 2-\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", - "invalid_2fa_method": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043c\u0435\u0442\u043e\u0434 2FA (\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430).", - "invalid_login": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0432\u043b\u0438\u0437\u0430\u043d\u0435, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "title": "\u0414\u0432\u0443-\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" - }, - "user": { - "data": { - "authorization_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f (\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0437\u0430 \u0440\u044a\u0447\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435)", - "email": "\u0418\u043c\u0435\u0439\u043b", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - }, - "title": "\u0412\u0445\u043e\u0434 \u0432 Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ca.json b/homeassistant/components/hangouts/translations/ca.json deleted file mode 100644 index 8b629201cc1..00000000000 --- a/homeassistant/components/hangouts/translations/ca.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El servei ja est\u00e0 configurat", - "unknown": "Error inesperat" - }, - "error": { - "invalid_2fa": "La verificaci\u00f3 en dos passos no \u00e9s v\u00e0lida, torna-ho a provar.", - "invalid_2fa_method": "M\u00e8tode 2FA inv\u00e0lid (verifica-ho al m\u00f2bil).", - "invalid_login": "L'inici de sessi\u00f3 no \u00e9s v\u00e0lid, torna-ho a provar." - }, - "step": { - "2fa": { - "data": { - "2fa": "PIN 2FA" - }, - "description": "Buit", - "title": "Verificaci\u00f3 en dos passos" - }, - "user": { - "data": { - "authorization_code": "Codi d'autoritzaci\u00f3 (necessari per a l'autenticaci\u00f3 manual)", - "email": "Correu electr\u00f2nic", - "password": "Contrasenya" - }, - "description": "Buit", - "title": "Inici de sessi\u00f3 de Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/cs.json b/homeassistant/components/hangouts/translations/cs.json deleted file mode 100644 index 11bef6d1d1a..00000000000 --- a/homeassistant/components/hangouts/translations/cs.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Slu\u017eba je ji\u017e nastavena", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "error": { - "invalid_2fa": "Dfoufaktorov\u00e9 ov\u011b\u0159en\u00ed se nezda\u0159ilo. Zkuste to znovu.", - "invalid_2fa_method": "Neplatn\u00e1 metoda 2FA (ov\u011b\u0159en\u00ed na telefonu).", - "invalid_login": "Neplatn\u00e9 p\u0159ihla\u0161ovac\u00ed jm\u00e9no, pros\u00edm zkuste to znovu." - }, - "step": { - "2fa": { - "data": { - "2fa": "Dvoufaktorov\u00fd ov\u011b\u0159ovac\u00ed k\u00f3d" - }, - "description": "Pr\u00e1zdn\u00e9", - "title": "Dvoufaktorov\u00e9 ov\u011b\u0159en\u00ed" - }, - "user": { - "data": { - "authorization_code": "Autoriza\u010dn\u00ed k\u00f3d (vy\u017eadov\u00e1n pro ru\u010dn\u00ed ov\u011b\u0159en\u00ed)", - "email": "E-mail", - "password": "Heslo" - }, - "description": "Pr\u00e1zdn\u00e9", - "title": "P\u0159ihl\u00e1\u0161en\u00ed do slu\u017eby Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/da.json b/homeassistant/components/hangouts/translations/da.json deleted file mode 100644 index e490c33805d..00000000000 --- a/homeassistant/components/hangouts/translations/da.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts er allerede konfigureret", - "unknown": "Ukendt fejl opstod" - }, - "error": { - "invalid_2fa": "Ugyldig tofaktor-godkendelse, pr\u00f8v igen.", - "invalid_2fa_method": "Ugyldig 2FA-metode (Bekr\u00e6ft p\u00e5 telefon).", - "invalid_login": "Ugyldig login, pr\u00f8v venligst igen." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA pin" - }, - "title": "Tofaktor-godkendelse" - }, - "user": { - "data": { - "authorization_code": "Godkendelseskode (kr\u00e6vet til manuel godkendelse)", - "email": "Emailadresse", - "password": "Adgangskode" - }, - "title": "Google Hangouts login" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json deleted file mode 100644 index 53225644a2d..00000000000 --- a/homeassistant/components/hangouts/translations/de.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert", - "unknown": "Unerwarteter Fehler" - }, - "error": { - "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.", - "invalid_2fa_method": "Ung\u00fcltige 2FA Methode (mit Telefon verifizieren)", - "invalid_login": "Ung\u00fcltiges Login, bitte versuche es erneut." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "description": "Leer", - "title": "2-Faktor-Authentifizierung" - }, - "user": { - "data": { - "authorization_code": "Autorisierungscode (f\u00fcr die manuelle Authentifizierung erforderlich)", - "email": "E-Mail", - "password": "Passwort" - }, - "description": "Leer", - "title": "Google Chat Anmeldung" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/el.json b/homeassistant/components/hangouts/translations/el.json deleted file mode 100644 index 4b453c5bbaa..00000000000 --- a/homeassistant/components/hangouts/translations/el.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "error": { - "invalid_2fa": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", - "invalid_2fa_method": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 2FA (\u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf).", - "invalid_login": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." - }, - "step": { - "2fa": { - "data": { - "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" - }, - "description": "\u039a\u03b5\u03bd\u03cc", - "title": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" - }, - "user": { - "data": { - "authorization_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 (\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2)", - "email": "Email", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u039a\u03b5\u03bd\u03cc", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/en.json b/homeassistant/components/hangouts/translations/en.json deleted file mode 100644 index 4829e843c6c..00000000000 --- a/homeassistant/components/hangouts/translations/en.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Service is already configured", - "unknown": "Unexpected error" - }, - "error": { - "invalid_2fa": "Invalid 2 Factor Authentication, please try again.", - "invalid_2fa_method": "Invalid 2FA Method (verify on Phone).", - "invalid_login": "Invalid Login, please try again." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "title": "2-Factor-Authentication" - }, - "user": { - "data": { - "authorization_code": "Authorization Code (required for manual authentication)", - "email": "Email", - "password": "Password" - }, - "title": "Google Chat Login" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/es-419.json b/homeassistant/components/hangouts/translations/es-419.json deleted file mode 100644 index a8ae41ec21e..00000000000 --- a/homeassistant/components/hangouts/translations/es-419.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts ya est\u00e1 configurado", - "unknown": "Se produjo un error desconocido." - }, - "error": { - "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, intente nuevamente.", - "invalid_2fa_method": "M\u00e9todo 2FA no v\u00e1lido (verificar en el tel\u00e9fono).", - "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." - }, - "step": { - "2fa": { - "data": { - "2fa": "Pin 2FA" - }, - "title": "Autenticaci\u00f3n de 2 factores" - }, - "user": { - "data": { - "authorization_code": "C\u00f3digo de autorizaci\u00f3n (requerido para la autenticaci\u00f3n manual)", - "email": "Direcci\u00f3n de correo electr\u00f3nico", - "password": "Contrase\u00f1a" - }, - "title": "Inicio de sesi\u00f3n de Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/es.json b/homeassistant/components/hangouts/translations/es.json deleted file mode 100644 index 29fe36ea23c..00000000000 --- a/homeassistant/components/hangouts/translations/es.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El servicio ya est\u00e1 configurado", - "unknown": "Error inesperado" - }, - "error": { - "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, por favor, int\u00e9ntalo de nuevo.", - "invalid_2fa_method": "M\u00e9todo 2FA no v\u00e1lido (verificar en el tel\u00e9fono).", - "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." - }, - "step": { - "2fa": { - "data": { - "2fa": "PIN 2FA" - }, - "title": "Autenticaci\u00f3n de 2 factores" - }, - "user": { - "data": { - "authorization_code": "C\u00f3digo de autorizaci\u00f3n (requerido para la autenticaci\u00f3n manual)", - "email": "Correo electr\u00f3nico", - "password": "Contrase\u00f1a" - }, - "title": "Inicio de sesi\u00f3n de Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json deleted file mode 100644 index 96ed8b998ec..00000000000 --- a/homeassistant/components/hangouts/translations/et.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Teenus on juba seadistatud", - "unknown": "Tundmatu viga" - }, - "error": { - "invalid_2fa": "Vale 2-teguriline autentimine, proovi uuesti.", - "invalid_2fa_method": "Kehtetu kaheastmelise tuvastuse meetod (kontrolli telefonistl).", - "invalid_login": "Vale kasutajanimi, palun proovi uuesti." - }, - "step": { - "2fa": { - "data": { - "2fa": "Kaheastmelise tuvastuse PIN" - }, - "description": "", - "title": "Kaheastmeline autentimine" - }, - "user": { - "data": { - "authorization_code": "Autoriseerimiskood (vajalik k\u00e4sitsi autentimiseks)", - "email": "E-posti aadress", - "password": "Salas\u00f5na" - }, - "description": "", - "title": "Google Chat'i sisselogimine" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/fi.json b/homeassistant/components/hangouts/translations/fi.json deleted file mode 100644 index e93642a952d..00000000000 --- a/homeassistant/components/hangouts/translations/fi.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "unknown": "Tapahtui tuntematon virhe." - }, - "error": { - "invalid_2fa": "Virheellinen kaksitekij\u00e4todennus, yrit\u00e4 uudelleen.", - "invalid_2fa_method": "Virheellinen 2FA-menetelm\u00e4 (tarkista puhelimessa).", - "invalid_login": "Virheellinen kirjautuminen, yrit\u00e4 uudelleen." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA-pin" - }, - "description": "Tyhj\u00e4", - "title": "Kaksivaiheinen tunnistus" - }, - "user": { - "data": { - "email": "S\u00e4hk\u00f6postiosoite", - "password": "Salasana" - }, - "description": "Tyhj\u00e4", - "title": "Google Hangouts -kirjautuminen" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/fr.json b/homeassistant/components/hangouts/translations/fr.json deleted file mode 100644 index 78a2517d29a..00000000000 --- a/homeassistant/components/hangouts/translations/fr.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", - "unknown": "Erreur inattendue" - }, - "error": { - "invalid_2fa": "Authentification \u00e0 deux facteurs non valide, veuillez r\u00e9essayer.", - "invalid_2fa_method": "M\u00e9thode 2FA non valide (v\u00e9rifiez sur le t\u00e9l\u00e9phone).", - "invalid_login": "Identifiant non valide, veuillez r\u00e9essayer." - }, - "step": { - "2fa": { - "data": { - "2fa": "Code NIP d'authentification \u00e0 2 facteurs" - }, - "description": "Vide", - "title": "Authentification \u00e0 2 facteurs" - }, - "user": { - "data": { - "authorization_code": "Code d'autorisation (requis pour l'authentification manuelle)", - "email": "Courriel", - "password": "Mot de passe" - }, - "description": "Vide", - "title": "Connexion \u00e0 Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/he.json b/homeassistant/components/hangouts/translations/he.json deleted file mode 100644 index ad696cad365..00000000000 --- a/homeassistant/components/hangouts/translations/he.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "error": { - "invalid_2fa": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d1\u05d1\u05e7\u05e9\u05d4 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", - "invalid_2fa_method": "\u05d3\u05e8\u05da \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea (\u05d0\u05de\u05ea \u05d1\u05d8\u05dc\u05e4\u05d5\u05df).", - "invalid_login": "\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." - }, - "step": { - "2fa": { - "data": { - "2fa": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" - }, - "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" - }, - "user": { - "data": { - "email": "\u05d3\u05d5\u05d0\"\u05dc", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05e6'\u05d0\u05d8 \u05e9\u05dc \u05d2\u05d5\u05d2\u05dc" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json deleted file mode 100644 index 1ea997aa098..00000000000 --- a/homeassistant/components/hangouts/translations/hu.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "error": { - "invalid_2fa": "\u00c9rv\u00e9nytelen K\u00e9tfaktoros hiteles\u00edt\u00e9s, pr\u00f3b\u00e1ld \u00fajra.", - "invalid_2fa_method": "\u00c9rv\u00e9nytelen 2FA M\u00f3dszer (Ellen\u0151rz\u00e9s a Telefonon).", - "invalid_login": "\u00c9rv\u00e9nytelen bejelentkez\u00e9s, pr\u00f3b\u00e1ld \u00fajra." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "description": "\u00dcres", - "title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s" - }, - "user": { - "data": { - "authorization_code": "Enged\u00e9lyez\u00e9si k\u00f3d (k\u00e9zi hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges)", - "email": "E-mail", - "password": "Jelsz\u00f3" - }, - "description": "\u00dcres", - "title": "Google Chat bejelentkez\u00e9s" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/id.json b/homeassistant/components/hangouts/translations/id.json deleted file mode 100644 index 2336b211c9e..00000000000 --- a/homeassistant/components/hangouts/translations/id.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Layanan sudah dikonfigurasi", - "unknown": "Kesalahan yang tidak diharapkan" - }, - "error": { - "invalid_2fa": "Autentikasi 2 Faktor Tidak Valid, coba lagi.", - "invalid_2fa_method": "Metode 2FA Tidak Valid (Verifikasikan di Ponsel).", - "invalid_login": "Info Masuk Tidak Valid, coba lagi." - }, - "step": { - "2fa": { - "data": { - "2fa": "PIN 2FA" - }, - "description": "Kosong", - "title": "Autentikasi Dua Faktor" - }, - "user": { - "data": { - "authorization_code": "Kode Otorisasi (diperlukan untuk autentikasi manual)", - "email": "Email", - "password": "Kata Sandi" - }, - "description": "Kosong", - "title": "Info Masuk Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json deleted file mode 100644 index 76d81a184d9..00000000000 --- a/homeassistant/components/hangouts/translations/it.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "unknown": "Errore imprevisto" - }, - "error": { - "invalid_2fa": "Autenticazione a 2 fattori non valida, riprova.", - "invalid_2fa_method": "Metodo 2FA non valido (verifica sul telefono).", - "invalid_login": "Accesso non valido, riprova." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "description": "Vuoto", - "title": "Autenticazione a due fattori" - }, - "user": { - "data": { - "authorization_code": "Codice di autorizzazione (necessario per l'autenticazione manuale)", - "email": "Email", - "password": "Password" - }, - "description": "Vuoto", - "title": "Accesso a Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ja.json b/homeassistant/components/hangouts/translations/ja.json deleted file mode 100644 index 14637ee1155..00000000000 --- a/homeassistant/components/hangouts/translations/ja.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "error": { - "invalid_2fa": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", - "invalid_2fa_method": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9(\u96fb\u8a71\u3067\u78ba\u8a8d)", - "invalid_login": "\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" - }, - "step": { - "2fa": { - "data": { - "2fa": "2\u8981\u7d20 PIN" - }, - "description": "\u7a7a", - "title": "2\u8981\u7d20\u8a8d\u8a3c" - }, - "user": { - "data": { - "authorization_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9(\u624b\u52d5\u8a8d\u8a3c\u6642\u306b\u5fc5\u8981)", - "email": "E\u30e1\u30fc\u30eb", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u7a7a", - "title": "Google \u30cf\u30f3\u30b0\u30a2\u30a6\u30c8 \u30ed\u30b0\u30a4\u30f3" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ko.json b/homeassistant/components/hangouts/translations/ko.json deleted file mode 100644 index 56c3c577a89..00000000000 --- a/homeassistant/components/hangouts/translations/ko.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "error": { - "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "invalid_2fa_method": "2\ub2e8\uacc4 \uc778\uc99d \ubc29\ubc95\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. (\uc804\ud654\uae30\uc5d0\uc11c \ud655\uc778)", - "invalid_login": "\ub85c\uadf8\uc778\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." - }, - "step": { - "2fa": { - "data": { - "2fa": "2\ub2e8\uacc4 \uc778\uc99d PIN" - }, - "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "2\ub2e8\uacc4 \uc778\uc99d" - }, - "user": { - "data": { - "authorization_code": "\uc778\uc99d \ucf54\ub4dc (\uc218\ub3d9 \uc778\uc99d\uc5d0 \ud544\uc694)", - "email": "\uc774\uba54\uc77c", - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "Google \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/lb.json b/homeassistant/components/hangouts/translations/lb.json deleted file mode 100644 index a91f2e7b040..00000000000 --- a/homeassistant/components/hangouts/translations/lb.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Service ass scho konfigur\u00e9iert", - "unknown": "Onerwaarte Feeler" - }, - "error": { - "invalid_2fa": "Ong\u00eblteg 2-Faktor Authentifikatioun, prob\u00e9iert w.e.g. nach emol.", - "invalid_2fa_method": "Ong\u00eblteg 2FA Methode (Iwwerpr\u00e9ift et um Telefon)", - "invalid_login": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA Pin" - }, - "description": "Eidel", - "title": "2-Faktor-Authentifikatioun" - }, - "user": { - "data": { - "authorization_code": "Autorisatioun's Code (n\u00e9ideg fir eng manuell Authentifikatioun)", - "email": "E-Mail", - "password": "Passwuert" - }, - "description": "Eidel", - "title": "Google Hangouts Login" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/lt.json b/homeassistant/components/hangouts/translations/lt.json deleted file mode 100644 index 13dbbf8bdbc..00000000000 --- a/homeassistant/components/hangouts/translations/lt.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "title": "2 veiksni\u0173 autentifikavimas" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/nl.json b/homeassistant/components/hangouts/translations/nl.json deleted file mode 100644 index 826bc560045..00000000000 --- a/homeassistant/components/hangouts/translations/nl.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dienst is al geconfigureerd", - "unknown": "Onverwachte fout" - }, - "error": { - "invalid_2fa": "Ongeldige twee-factor-authenticatie, probeer het opnieuw.", - "invalid_2fa_method": "Ongeldige 2FA-methode (verifi\u00ebren op telefoon).", - "invalid_login": "Ongeldige aanmelding, probeer het opnieuw." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA pin" - }, - "description": "Leeg", - "title": "Twee-factor-authenticatie" - }, - "user": { - "data": { - "authorization_code": "Autorisatiecode (vereist voor handmatige authenticatie)", - "email": "E-mail", - "password": "Wachtwoord" - }, - "description": "Leeg", - "title": "Google Chat-login" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/nn.json b/homeassistant/components/hangouts/translations/nn.json deleted file mode 100644 index 883a53441af..00000000000 --- a/homeassistant/components/hangouts/translations/nn.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts er allereie konfigurert", - "unknown": "Det hende ein ukjent feil" - }, - "error": { - "invalid_2fa": "Ugyldig to-faktor-autentisering. Ver vennleg og pr\u00f8v igjen.", - "invalid_2fa_method": "Ugyldig 2FA-metode (godkjenn p\u00e5 telefonen).", - "invalid_login": "Ugyldig innlogging. Pr\u00f8v igjen." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "title": "To-faktor-autentisering" - }, - "user": { - "data": { - "email": "Epostadresse", - "password": "Passord" - }, - "title": "Google Hangouts Login" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json deleted file mode 100644 index 751d54c852e..00000000000 --- a/homeassistant/components/hangouts/translations/no.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Tjenesten er allerede konfigurert", - "unknown": "Uventet feil" - }, - "error": { - "invalid_2fa": "Ugyldig totrinnsbekreftelse, vennligst pr\u00f8v igjen.", - "invalid_2fa_method": "Ugyldig 2FA-metode (bekreft p\u00e5 telefon).", - "invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN" - }, - "description": "", - "title": "Totrinnsbekreftelse" - }, - "user": { - "data": { - "authorization_code": "Godkjenningskode (kreves for manuell godkjenning)", - "email": "E-post", - "password": "Passord" - }, - "description": "", - "title": "Google Chat-p\u00e5logging" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json deleted file mode 100644 index 4dafdc5d996..00000000000 --- a/homeassistant/components/hangouts/translations/pl.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "error": { - "invalid_2fa": "Nieprawid\u0142owe uwierzytelnienie dwusk\u0142adnikowe, spr\u00f3buj ponownie", - "invalid_2fa_method": "Nieprawid\u0142owa metoda uwierzytelniania dwusk\u0142adnikowego (u\u017cyj weryfikacji przez telefon)", - "invalid_login": "Nieprawid\u0142owy login, spr\u00f3buj ponownie" - }, - "step": { - "2fa": { - "data": { - "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" - }, - "description": "Pusty", - "title": "Uwierzytelnianie dwusk\u0142adnikowe" - }, - "user": { - "data": { - "authorization_code": "Kod autoryzacji (wymagany do r\u0119cznego uwierzytelnienia)", - "email": "Adres e-mail", - "password": "Has\u0142o" - }, - "description": "Pusty", - "title": "Logowanie do Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/pt-BR.json b/homeassistant/components/hangouts/translations/pt-BR.json deleted file mode 100644 index 9e4d04cd989..00000000000 --- a/homeassistant/components/hangouts/translations/pt-BR.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", - "unknown": "Erro inesperado" - }, - "error": { - "invalid_2fa": "Autentica\u00e7\u00e3o de 2 fatores inv\u00e1lida, por favor, tente novamente.", - "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).", - "invalid_login": "Login inv\u00e1lido, por favor, tente novamente." - }, - "step": { - "2fa": { - "data": { - "2fa": "C\u00f3digo 2FA" - }, - "description": "Vazio", - "title": "Autentica\u00e7\u00e3o de 2 Fatores" - }, - "user": { - "data": { - "authorization_code": "C\u00f3digo de Autoriza\u00e7\u00e3o (requerido para autentica\u00e7\u00e3o manual)", - "email": "Email", - "password": "Senha" - }, - "description": "Vazio", - "title": "Login no Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/pt.json b/homeassistant/components/hangouts/translations/pt.json deleted file mode 100644 index b4feb91c76d..00000000000 --- a/homeassistant/components/hangouts/translations/pt.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts j\u00e1 est\u00e1 configurado", - "unknown": "Erro inesperado" - }, - "error": { - "invalid_2fa": "Autentica\u00e7\u00e3o por 2 fatores inv\u00e1lida, por favor, tente novamente.", - "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).", - "invalid_login": "Login inv\u00e1lido, por favor, tente novamente." - }, - "step": { - "2fa": { - "data": { - "2fa": "Pin 2FA" - }, - "description": "Vazio", - "title": "Autentica\u00e7\u00e3o de 2 Fatores" - }, - "user": { - "data": { - "email": "E-mail", - "password": "Palavra-passe" - }, - "description": "Vazio", - "title": "Login Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ro.json b/homeassistant/components/hangouts/translations/ro.json deleted file mode 100644 index 682d561929c..00000000000 --- a/homeassistant/components/hangouts/translations/ro.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts este deja configurat", - "unknown": "Sa produs o eroare necunoscut\u0103." - }, - "error": { - "invalid_2fa_method": "Metoda 2FA invalid\u0103 (Verifica\u021bi pe telefon).", - "invalid_login": "Conectare invalid\u0103, \u00eencerca\u021bi din nou." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA Pin" - } - }, - "user": { - "data": { - "email": "Adresa de email", - "password": "Parol\u0103" - }, - "description": "Gol", - "title": "Conectare Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ru.json b/homeassistant/components/hangouts/translations/ru.json deleted file mode 100644 index 781e1e25eef..00000000000 --- a/homeassistant/components/hangouts/translations/ru.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "error": { - "invalid_2fa": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", - "invalid_2fa_method": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 (\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435).", - "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." - }, - "step": { - "2fa": { - "data": { - "2fa": "\u041f\u0438\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" - }, - "description": "\u043f\u0443\u0441\u0442\u043e", - "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" - }, - "user": { - "data": { - "authorization_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438)", - "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" - }, - "description": "\u043f\u0443\u0441\u0442\u043e", - "title": "Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/sl.json b/homeassistant/components/hangouts/translations/sl.json deleted file mode 100644 index 853dfa1487a..00000000000 --- a/homeassistant/components/hangouts/translations/sl.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts je \u017ee konfiguriran", - "unknown": "Pri\u0161lo je do neznane napake" - }, - "error": { - "invalid_2fa": "Neveljavna 2FA avtorizacija, prosimo, poskusite znova.", - "invalid_2fa_method": "Neveljavna 2FA Metoda (Preverite na Telefonu).", - "invalid_login": "Neveljavna Prijava, prosimo, poskusite znova." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA Pin" - }, - "description": "prazno", - "title": "Dvofaktorska avtorizacija" - }, - "user": { - "data": { - "authorization_code": "Koda pooblastila (potrebna za ro\u010dno overjanje)", - "email": "E-po\u0161tni naslov", - "password": "Geslo" - }, - "description": "prazno", - "title": "Prijava za Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/sv.json b/homeassistant/components/hangouts/translations/sv.json deleted file mode 100644 index f9e5ec14c54..00000000000 --- a/homeassistant/components/hangouts/translations/sv.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts \u00e4r redan inst\u00e4llt", - "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade" - }, - "error": { - "invalid_2fa": "Ogiltig 2FA autentisering, f\u00f6rs\u00f6k igen.", - "invalid_2fa_method": "Ogiltig 2FA-metod (Verifiera med telefon).", - "invalid_login": "Ogiltig inloggning, f\u00f6rs\u00f6k igen." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA Pinkod" - }, - "description": "Missing english translation", - "title": "Tv\u00e5faktorsautentisering" - }, - "user": { - "data": { - "authorization_code": "Auktoriseringskod (kr\u00e4vs vid manuell verifiering)", - "email": "E-postadress", - "password": "L\u00f6senord" - }, - "description": "Missing english translation", - "title": "Google Hangouts-inloggning" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/th.json b/homeassistant/components/hangouts/translations/th.json deleted file mode 100644 index bcc59392e2e..00000000000 --- a/homeassistant/components/hangouts/translations/th.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "step": { - "2fa": { - "title": "\u0e23\u0e2b\u0e31\u0e2a\u0e23\u0e31\u0e1a\u0e23\u0e2d\u0e07\u0e04\u0e27\u0e32\u0e21\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07\u0e2a\u0e2d\u0e07\u0e1b\u0e31\u0e08\u0e08\u0e31\u0e22" - }, - "user": { - "data": { - "email": "\u0e17\u0e35\u0e48\u0e2d\u0e22\u0e39\u0e48\u0e2d\u0e35\u0e40\u0e21\u0e25", - "password": "\u0e23\u0e2b\u0e31\u0e2a\u0e1c\u0e48\u0e32\u0e19" - }, - "description": "\u0e27\u0e48\u0e32\u0e07\u0e40\u0e1b\u0e25\u0e48\u0e32" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/tr.json b/homeassistant/components/hangouts/translations/tr.json deleted file mode 100644 index 5ddf2a64cbb..00000000000 --- a/homeassistant/components/hangouts/translations/tr.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "unknown": "Beklenmeyen hata" - }, - "error": { - "invalid_2fa": "Ge\u00e7ersiz 2 Fakt\u00f6rl\u00fc Kimlik Do\u011frulama, l\u00fctfen tekrar deneyin.", - "invalid_2fa_method": "Ge\u00e7ersiz 2FA Y\u00f6ntemi (Telefonda do\u011frulay\u0131n).", - "invalid_login": "Ge\u00e7ersiz Giri\u015f, l\u00fctfen tekrar deneyin." - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA PIN'i" - }, - "description": "Bo\u015f", - "title": "2-Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" - }, - "user": { - "data": { - "authorization_code": "Yetkilendirme Kodu (manuel kimlik do\u011frulama i\u00e7in gereklidir)", - "email": "E-posta", - "password": "Parola" - }, - "description": "Bo\u015f", - "title": "Google Sohbet Giri\u015fi" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/uk.json b/homeassistant/components/hangouts/translations/uk.json deleted file mode 100644 index 93eb699d37c..00000000000 --- a/homeassistant/components/hangouts/translations/uk.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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/hangouts/translations/vi.json b/homeassistant/components/hangouts/translations/vi.json deleted file mode 100644 index d794a0b5afa..00000000000 --- a/homeassistant/components/hangouts/translations/vi.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "invalid_2fa_method": "Ph\u01b0\u01a1ng ph\u00e1p 2FA kh\u00f4ng h\u1ee3p l\u1ec7 (X\u00e1c minh tr\u00ean \u0111i\u1ec7n tho\u1ea1i)." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/zh-Hans.json b/homeassistant/components/hangouts/translations/zh-Hans.json deleted file mode 100644 index 46d1de99c73..00000000000 --- a/homeassistant/components/hangouts/translations/zh-Hans.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Google Hangouts \u5df2\u914d\u7f6e\u5b8c\u6210", - "unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002" - }, - "error": { - "invalid_2fa": "\u53cc\u91cd\u8ba4\u8bc1\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002", - "invalid_2fa_method": "\u65e0\u6548\u7684\u53cc\u91cd\u8ba4\u8bc1\u65b9\u6cd5\uff08\u7535\u8bdd\u9a8c\u8bc1\uff09\u3002", - "invalid_login": "\u767b\u9646\u5931\u8d25\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002" - }, - "step": { - "2fa": { - "data": { - "2fa": "2FA Pin" - }, - "description": "\u65e0", - "title": "\u53cc\u91cd\u8ba4\u8bc1" - }, - "user": { - "data": { - "email": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740", - "password": "\u5bc6\u7801" - }, - "description": "\u65e0", - "title": "\u767b\u5f55 Google Hangouts" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/zh-Hant.json b/homeassistant/components/hangouts/translations/zh-Hant.json deleted file mode 100644 index f9884d5e214..00000000000 --- a/homeassistant/components/hangouts/translations/zh-Hant.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "error": { - "invalid_2fa": "\u96d9\u91cd\u8a8d\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", - "invalid_2fa_method": "\u5169\u968e\u6bb5\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002", - "invalid_login": "\u767b\u5165\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" - }, - "step": { - "2fa": { - "data": { - "2fa": "\u5169\u968e\u6bb5\u8a8d\u8b49\u78bc" - }, - "description": "\u7a7a\u767d", - "title": "\u96d9\u91cd\u8a8d\u8b49" - }, - "user": { - "data": { - "authorization_code": "\u9a57\u8b49\u78bc\uff08\u624b\u52d5\u9a57\u8b49\u5fc5\u9808\uff09", - "email": "\u96fb\u5b50\u90f5\u4ef6", - "password": "\u5bc6\u78bc" - }, - "description": "\u7a7a\u767d", - "title": "\u767b\u5165 Google Chat" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py index 47ff5830a84..eca599960f8 100644 --- a/homeassistant/components/hardkernel/hardware.py +++ b/homeassistant/components/hardkernel/hardware.py @@ -27,6 +27,10 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: if not board.startswith("odroid"): raise HomeAssistantError + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + return [ HardwareInfo( board=BoardInfo( @@ -35,6 +39,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: model=board, revision=None, ), + config_entries=config_entries, dongle=None, name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), url=None, diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 8ce5e7be7f3..801bc9b923a 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -34,6 +34,7 @@ class HardwareInfo: name: str | None board: BoardInfo | None + config_entries: list[str] | None dongle: USBInfo | None url: str | None diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index f222d4bd739..17b3d0c5717 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -66,18 +66,17 @@ class HkAvrDevice(MediaPlayerEntity): self._source_list = avr.sources - self._state = None self._muted = avr.muted self._current_source = avr.current_source def update(self) -> None: """Update the state of this media_player.""" if self._avr.is_on(): - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON elif self._avr.is_off(): - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF else: - self._state = None + self._attr_state = None self._muted = self._avr.muted self._current_source = self._avr.current_source @@ -87,11 +86,6 @@ class HkAvrDevice(MediaPlayerEntity): """Return the name of the device.""" return self._name - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def is_volume_muted(self): """Muted status not available.""" diff --git a/homeassistant/components/harmony/select.py b/homeassistant/components/harmony/select.py index 3728c4b17a4..a3f059d2c00 100644 --- a/homeassistant/components/harmony/select.py +++ b/homeassistant/components/harmony/select.py @@ -16,6 +16,8 @@ from .subscriber import HarmonyCallback _LOGGER = logging.getLogger(__name__) +TRANSLATABLE_POWER_OFF = "power_off" + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -31,6 +33,8 @@ async def async_setup_entry( class HarmonyActivitySelect(HarmonyEntity, SelectEntity): """Select representation of a Harmony activities.""" + _attr_device_class = f"{DOMAIN}__activities" + def __init__(self, name: str, data: HarmonyData) -> None: """Initialize HarmonyActivitySelect class.""" super().__init__(data=data) @@ -42,23 +46,27 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): @property def icon(self) -> str: """Return a representative icon.""" - if not self.available or self.current_option == ACTIVITY_POWER_OFF: + if not self.available or self.current_option == TRANSLATABLE_POWER_OFF: return "mdi:remote-tv-off" return "mdi:remote-tv" @property def options(self) -> list[str]: """Return a set of selectable options.""" - return [ACTIVITY_POWER_OFF] + sorted(self._data.activity_names) + return [TRANSLATABLE_POWER_OFF] + sorted(self._data.activity_names) @property def current_option(self) -> str | None: """Return the current activity.""" _, activity_name = self._data.current_activity + if activity_name == ACTIVITY_POWER_OFF: + return TRANSLATABLE_POWER_OFF return activity_name async def async_select_option(self, option: str) -> None: """Change the current activity.""" + if option == TRANSLATABLE_POWER_OFF: + await self._data.async_start_activity(ACTIVITY_POWER_OFF) await self._data.async_start_activity(option) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/harmony/strings.select.json b/homeassistant/components/harmony/strings.select.json new file mode 100644 index 00000000000..5dbdf1a1c3d --- /dev/null +++ b/homeassistant/components/harmony/strings.select.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Power Off" + } + } +} diff --git a/homeassistant/components/harmony/translations/ca.json b/homeassistant/components/harmony/translations/ca.json index e5a15705bac..f5648d99dba 100644 --- a/homeassistant/components/harmony/translations/ca.json +++ b/homeassistant/components/harmony/translations/ca.json @@ -29,7 +29,7 @@ "activity": "Activitat predeterminada a executar quan no se n'especifica cap.", "delay_secs": "Retard entre l'enviament d'ordres." }, - "description": "Ajusta les opcions de Harmony Hub" + "description": "Ajusta les opcions d'Harmony Hub" } } } diff --git a/homeassistant/components/harmony/translations/select.ca.json b/homeassistant/components/harmony/translations/select.ca.json new file mode 100644 index 00000000000..2c11e9affa5 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.ca.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Apaga" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.cs.json b/homeassistant/components/harmony/translations/select.cs.json new file mode 100644 index 00000000000..c3a5be2a9dc --- /dev/null +++ b/homeassistant/components/harmony/translations/select.cs.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Vypnout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.de.json b/homeassistant/components/harmony/translations/select.de.json new file mode 100644 index 00000000000..4cb96d0a72c --- /dev/null +++ b/homeassistant/components/harmony/translations/select.de.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Ausschalten" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.el.json b/homeassistant/components/harmony/translations/select.el.json new file mode 100644 index 00000000000..72243ee6e28 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.el.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.en.json b/homeassistant/components/harmony/translations/select.en.json new file mode 100644 index 00000000000..539a1c14c21 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.en.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Power Off" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.es.json b/homeassistant/components/harmony/translations/select.es.json new file mode 100644 index 00000000000..23b2c4d5b91 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.es.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Apagar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.et.json b/homeassistant/components/harmony/translations/select.et.json new file mode 100644 index 00000000000..47802c3f211 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.et.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Toide v\u00e4lja" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.fr.json b/homeassistant/components/harmony/translations/select.fr.json new file mode 100644 index 00000000000..cf895f7fad6 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.fr.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "\u00c9teindre" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.id.json b/homeassistant/components/harmony/translations/select.id.json new file mode 100644 index 00000000000..8fb17a3cbec --- /dev/null +++ b/homeassistant/components/harmony/translations/select.id.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Daya Mati" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.it.json b/homeassistant/components/harmony/translations/select.it.json new file mode 100644 index 00000000000..4f6842c3c9b --- /dev/null +++ b/homeassistant/components/harmony/translations/select.it.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Spegni" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.nl.json b/homeassistant/components/harmony/translations/select.nl.json new file mode 100644 index 00000000000..48d4e0a8ff2 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.nl.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Uitschakelen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.no.json b/homeassistant/components/harmony/translations/select.no.json new file mode 100644 index 00000000000..c26762bd22f --- /dev/null +++ b/homeassistant/components/harmony/translations/select.no.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Sl\u00e5 av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.pl.json b/homeassistant/components/harmony/translations/select.pl.json new file mode 100644 index 00000000000..6786518c447 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.pl.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "wy\u0142\u0105czony" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.pt-BR.json b/homeassistant/components/harmony/translations/select.pt-BR.json new file mode 100644 index 00000000000..cd353b00716 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.pt-BR.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Desligar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.ru.json b/homeassistant/components/harmony/translations/select.ru.json new file mode 100644 index 00000000000..4902ddbb823 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.ru.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.sk.json b/homeassistant/components/harmony/translations/select.sk.json new file mode 100644 index 00000000000..b5f207e1ca1 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "Vypn\u00fa\u0165" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.zh-Hans.json b/homeassistant/components/harmony/translations/select.zh-Hans.json new file mode 100644 index 00000000000..ee5367ba9c6 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.zh-Hans.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "\u5173\u95ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/select.zh-Hant.json b/homeassistant/components/harmony/translations/select.zh-Hant.json new file mode 100644 index 00000000000..622264706e7 --- /dev/null +++ b/homeassistant/components/harmony/translations/select.zh-Hant.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "\u95dc\u6a5f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/sk.json b/homeassistant/components/harmony/translations/sk.json new file mode 100644 index 00000000000..78cba980f6a --- /dev/null +++ b/homeassistant/components/harmony/translations/sk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "link": { + "description": "Chcete nastavi\u0165 {name} ({host})?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Predvolen\u00e1 aktivita, ktor\u00e1 sa m\u00e1 vykona\u0165, ke\u010f nie je zadan\u00e1 \u017eiadna." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index c811b35812e..581ed0e3292 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -47,6 +47,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow +from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState # noqa: F401 from .addon_panel import async_setup_addon_panel from .auth import async_setup_auth_view from .const import ( @@ -55,7 +56,6 @@ from .const import ( ATTR_AUTO_UPDATE, ATTR_CHANGELOG, ATTR_COMPRESSED, - ATTR_DISCOVERY, ATTR_FOLDERS, ATTR_HOMEASSISTANT, ATTR_INPUT, @@ -74,7 +74,25 @@ from .const import ( SupervisorEntityModel, ) from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F401 -from .handler import HassIO, HassioAPIError, api_data +from .handler import ( # noqa: F401 + HassIO, + HassioAPIError, + async_create_backup, + async_get_addon_discovery_info, + async_get_addon_info, + async_get_addon_store_info, + async_install_addon, + async_restart_addon, + async_set_addon_options, + async_start_addon, + async_stop_addon, + async_uninstall_addon, + async_update_addon, + async_update_core, + async_update_diagnostics, + async_update_os, + async_update_supervisor, +) from .http import HassIOView from .ingress import async_setup_ingress_view from .repairs import SupervisorRepairs @@ -221,202 +239,6 @@ HARDWARE_INTEGRATIONS = { } -@bind_hass -async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: - """Return add-on info. - - The add-on must be installed. - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - return await hassio.get_addon_info(slug) - - -@api_data -async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: - """Return add-on store info. - - The caller of the function should handle HassioAPIError. - """ - hassio: HassIO = hass.data[DOMAIN] - command = f"/store/addons/{slug}" - return await hassio.send_command(command, method="get") - - -@bind_hass -async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: - """Update Supervisor diagnostics toggle. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - return await hassio.update_diagnostics(diagnostics) - - -@bind_hass -@api_data -async def async_install_addon(hass: HomeAssistant, slug: str) -> dict: - """Install add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/install" - return await hassio.send_command(command, timeout=None) - - -@bind_hass -@api_data -async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: - """Uninstall add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/uninstall" - return await hassio.send_command(command, timeout=60) - - -@bind_hass -@api_data -async def async_update_addon( - hass: HomeAssistant, - slug: str, - backup: bool = False, -) -> dict: - """Update add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/update" - return await hassio.send_command( - command, - payload={"backup": backup}, - timeout=None, - ) - - -@bind_hass -@api_data -async def async_start_addon(hass: HomeAssistant, slug: str) -> dict: - """Start add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/start" - return await hassio.send_command(command, timeout=60) - - -@bind_hass -@api_data -async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict: - """Restart add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/restart" - return await hassio.send_command(command, timeout=None) - - -@bind_hass -@api_data -async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict: - """Stop add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/stop" - return await hassio.send_command(command, timeout=60) - - -@bind_hass -@api_data -async def async_set_addon_options( - hass: HomeAssistant, slug: str, options: dict -) -> dict: - """Set add-on options. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/options" - return await hassio.send_command(command, payload=options) - - -@bind_hass -async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None: - """Return discovery data for an add-on.""" - hassio = hass.data[DOMAIN] - data = await hassio.retrieve_discovery_messages() - discovered_addons = data[ATTR_DISCOVERY] - return next((addon for addon in discovered_addons if addon["addon"] == slug), None) - - -@bind_hass -@api_data -async def async_create_backup( - hass: HomeAssistant, payload: dict, partial: bool = False -) -> dict: - """Create a full or partial backup. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - backup_type = "partial" if partial else "full" - command = f"/backups/new/{backup_type}" - return await hassio.send_command(command, payload=payload, timeout=None) - - -@bind_hass -@api_data -async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict: - """Update Home Assistant Operating System. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = "/os/update" - return await hassio.send_command( - command, - payload={"version": version}, - timeout=None, - ) - - -@bind_hass -@api_data -async def async_update_supervisor(hass: HomeAssistant) -> dict: - """Update Home Assistant Supervisor. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = "/supervisor/update" - return await hassio.send_command(command, timeout=None) - - -@bind_hass -@api_data -async def async_update_core( - hass: HomeAssistant, version: str | None = None, backup: bool = False -) -> dict: - """Update Home Assistant Core. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = "/core/update" - return await hassio.send_command( - command, - payload={"version": version, "backup": backup}, - timeout=None, - ) - - @callback @bind_hass def get_info(hass): @@ -449,7 +271,7 @@ def get_store(hass): @callback @bind_hass -def get_supervisor_info(hass): +def get_supervisor_info(hass: HomeAssistant) -> dict[str, Any] | None: """Return Supervisor information. Async friendly. @@ -897,7 +719,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(f"Error on Supervisor API: {err}") from err new_data: dict[str, Any] = {} - supervisor_info = get_supervisor_info(self.hass) + supervisor_info = get_supervisor_info(self.hass) or {} addons_info = get_addons_info(self.hass) addons_stats = get_addons_stats(self.hass) addons_changelogs = get_addons_changelogs(self.hass) @@ -1044,6 +866,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): log_failures: bool = True, raise_on_auth_failed: bool = False, scheduled: bool = False, + raise_on_entry_error: bool = False, ) -> None: """Refresh data.""" if not scheduled: @@ -1052,4 +875,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): await self.hassio.refresh_updates() except HassioAPIError as err: _LOGGER.warning("Error on Supervisor API: %s", err) - await super()._async_refresh(log_failures, raise_on_auth_failed, scheduled) + await super()._async_refresh( + log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error + ) diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py new file mode 100644 index 00000000000..f240937c7f5 --- /dev/null +++ b/homeassistant/components/hassio/addon_manager.py @@ -0,0 +1,376 @@ +"""Provide add-on management.""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable, Coroutine +from dataclasses import dataclass +from enum import Enum +from functools import partial, wraps +import logging +from typing import Any, TypeVar + +from typing_extensions import Concatenate, ParamSpec + +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .handler import ( + HassioAPIError, + async_create_backup, + async_get_addon_discovery_info, + async_get_addon_info, + async_get_addon_store_info, + async_install_addon, + async_restart_addon, + async_set_addon_options, + async_start_addon, + async_stop_addon, + async_uninstall_addon, + async_update_addon, +) + +_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager") +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def api_error( + error_message: str, +) -> Callable[ + [Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]], + Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]], +]: + """Handle HassioAPIError and raise a specific AddonError.""" + + def handle_hassio_api_error( + func: Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]] + ) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]: + """Handle a HassioAPIError.""" + + @wraps(func) + async def wrapper( + self: _AddonManagerT, *args: _P.args, **kwargs: _P.kwargs + ) -> _R: + """Wrap an add-on manager method.""" + try: + return_value = await func(self, *args, **kwargs) + except HassioAPIError as err: + raise AddonError( + f"{error_message.format(addon_name=self.addon_name)}: {err}" + ) from err + + return return_value + + return wrapper + + return handle_hassio_api_error + + +@dataclass +class AddonInfo: + """Represent the current add-on info state.""" + + hostname: str | None + options: dict[str, Any] + state: AddonState + update_available: bool + version: str | None + + +class AddonState(Enum): + """Represent the current state of the add-on.""" + + NOT_INSTALLED = "not_installed" + INSTALLING = "installing" + UPDATING = "updating" + NOT_RUNNING = "not_running" + RUNNING = "running" + + +class AddonManager: + """Manage the add-on. + + Methods may raise AddonError. + Only one instance of this class may exist per add-on + to keep track of running add-on tasks. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + addon_name: str, + addon_slug: str, + ) -> None: + """Set up the add-on manager.""" + self.addon_name = addon_name + self.addon_slug = addon_slug + self._hass = hass + self._logger = logger + self._install_task: asyncio.Task | None = None + self._restart_task: asyncio.Task | None = None + self._start_task: asyncio.Task | None = None + self._update_task: asyncio.Task | None = None + + def task_in_progress(self) -> bool: + """Return True if any of the add-on tasks are in progress.""" + return any( + task and not task.done() + for task in ( + self._restart_task, + self._install_task, + self._start_task, + self._update_task, + ) + ) + + @api_error("Failed to get the {addon_name} add-on discovery info") + async def async_get_addon_discovery_info(self) -> dict: + """Return add-on discovery info.""" + discovery_info = await async_get_addon_discovery_info( + self._hass, self.addon_slug + ) + + if not discovery_info: + raise AddonError(f"Failed to get {self.addon_name} add-on discovery info") + + discovery_info_config: dict = discovery_info["config"] + return discovery_info_config + + @api_error("Failed to get the {addon_name} add-on info") + async def async_get_addon_info(self) -> AddonInfo: + """Return and cache manager add-on info.""" + addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug) + self._logger.debug("Add-on store info: %s", addon_store_info) + if not addon_store_info["installed"]: + return AddonInfo( + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + addon_info = await async_get_addon_info(self._hass, self.addon_slug) + addon_state = self.async_get_addon_state(addon_info) + return AddonInfo( + hostname=addon_info["hostname"], + options=addon_info["options"], + state=addon_state, + update_available=addon_info["update_available"], + version=addon_info["version"], + ) + + @callback + def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: + """Return the current state of the managed add-on.""" + addon_state = AddonState.NOT_RUNNING + + if addon_info["state"] == "started": + addon_state = AddonState.RUNNING + if self._install_task and not self._install_task.done(): + addon_state = AddonState.INSTALLING + if self._update_task and not self._update_task.done(): + addon_state = AddonState.UPDATING + + return addon_state + + @api_error("Failed to set the {addon_name} add-on options") + async def async_set_addon_options(self, config: dict) -> None: + """Set manager add-on options.""" + options = {"options": config} + await async_set_addon_options(self._hass, self.addon_slug, options) + + @api_error("Failed to install the {addon_name} add-on") + async def async_install_addon(self) -> None: + """Install the managed add-on.""" + await async_install_addon(self._hass, self.addon_slug) + + @api_error("Failed to uninstall the {addon_name} add-on") + async def async_uninstall_addon(self) -> None: + """Uninstall the managed add-on.""" + await async_uninstall_addon(self._hass, self.addon_slug) + + @api_error("Failed to update the {addon_name} add-on") + async def async_update_addon(self) -> None: + """Update the managed add-on if needed.""" + addon_info = await self.async_get_addon_info() + + if addon_info.state is AddonState.NOT_INSTALLED: + raise AddonError(f"{self.addon_name} add-on is not installed") + + if not addon_info.update_available: + return + + await self.async_create_backup() + await async_update_addon(self._hass, self.addon_slug) + + @api_error("Failed to start the {addon_name} add-on") + async def async_start_addon(self) -> None: + """Start the managed add-on.""" + await async_start_addon(self._hass, self.addon_slug) + + @api_error("Failed to restart the {addon_name} add-on") + async def async_restart_addon(self) -> None: + """Restart the managed add-on.""" + await async_restart_addon(self._hass, self.addon_slug) + + @api_error("Failed to stop the {addon_name} add-on") + async def async_stop_addon(self) -> None: + """Stop the managed add-on.""" + await async_stop_addon(self._hass, self.addon_slug) + + @api_error("Failed to create a backup of the {addon_name} add-on") + async def async_create_backup(self) -> None: + """Create a partial backup of the managed add-on.""" + addon_info = await self.async_get_addon_info() + name = f"addon_{self.addon_slug}_{addon_info.version}" + + self._logger.debug("Creating backup: %s", name) + await async_create_backup( + self._hass, + {"name": name, "addons": [self.addon_slug]}, + partial=True, + ) + + async def async_configure_addon( + self, + addon_config: dict[str, Any], + ) -> None: + """Configure the manager add-on, if needed.""" + addon_info = await self.async_get_addon_info() + + if addon_info.state is AddonState.NOT_INSTALLED: + raise AddonError(f"{self.addon_name} add-on is not installed") + + if addon_config != addon_info.options: + await self.async_set_addon_options(addon_config) + + @callback + def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that installs the managed add-on. + + Only schedule a new install task if the there's no running task. + """ + if not self._install_task or self._install_task.done(): + self._logger.info( + "%s add-on is not installed. Installing add-on", self.addon_name + ) + self._install_task = self._async_schedule_addon_operation( + self.async_install_addon, catch_error=catch_error + ) + return self._install_task + + @callback + def async_schedule_install_setup_addon( + self, + addon_config: dict[str, Any], + catch_error: bool = False, + ) -> asyncio.Task: + """Schedule a task that installs and sets up the managed add-on. + + Only schedule a new install task if the there's no running task. + """ + if not self._install_task or self._install_task.done(): + self._logger.info( + "%s add-on is not installed. Installing add-on", self.addon_name + ) + self._install_task = self._async_schedule_addon_operation( + self.async_install_addon, + partial( + self.async_configure_addon, + addon_config, + ), + self.async_start_addon, + catch_error=catch_error, + ) + return self._install_task + + @callback + def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that updates and sets up the managed add-on. + + Only schedule a new update task if the there's no running task. + """ + if not self._update_task or self._update_task.done(): + self._logger.info("Trying to update the %s add-on", self.addon_name) + self._update_task = self._async_schedule_addon_operation( + self.async_update_addon, + catch_error=catch_error, + ) + return self._update_task + + @callback + def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that starts the managed add-on. + + Only schedule a new start task if the there's no running task. + """ + if not self._start_task or self._start_task.done(): + self._logger.info( + "%s add-on is not running. Starting add-on", self.addon_name + ) + self._start_task = self._async_schedule_addon_operation( + self.async_start_addon, catch_error=catch_error + ) + return self._start_task + + @callback + def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that restarts the managed add-on. + + Only schedule a new restart task if the there's no running task. + """ + if not self._restart_task or self._restart_task.done(): + self._logger.info("Restarting %s add-on", self.addon_name) + self._restart_task = self._async_schedule_addon_operation( + self.async_restart_addon, catch_error=catch_error + ) + return self._restart_task + + @callback + def async_schedule_setup_addon( + self, + addon_config: dict[str, Any], + catch_error: bool = False, + ) -> asyncio.Task: + """Schedule a task that configures and starts the managed add-on. + + Only schedule a new setup task if there's no running task. + """ + if not self._start_task or self._start_task.done(): + self._logger.info( + "%s add-on is not running. Starting add-on", self.addon_name + ) + self._start_task = self._async_schedule_addon_operation( + partial( + self.async_configure_addon, + addon_config, + ), + self.async_start_addon, + catch_error=catch_error, + ) + return self._start_task + + @callback + def _async_schedule_addon_operation( + self, *funcs: Callable, catch_error: bool = False + ) -> asyncio.Task: + """Schedule an add-on task.""" + + async def addon_operation() -> None: + """Do the add-on operation and catch AddonError.""" + for func in funcs: + try: + await func() + except AddonError as err: + if not catch_error: + raise + self._logger.error(err) + break + + return self._hass.async_create_task(addon_operation()) + + +class AddonError(HomeAssistantError): + """Represent an error with the managed add-on.""" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index ee16bdf8158..4f300ef16db 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -1,4 +1,6 @@ """Handler for Hass.io.""" +from __future__ import annotations + import asyncio from http import HTTPStatus import logging @@ -12,6 +14,10 @@ from homeassistant.components.http import ( CONF_SSL_CERTIFICATE, ) from homeassistant.const import SERVER_PORT +from homeassistant.core import HomeAssistant +from homeassistant.loader import bind_hass + +from .const import ATTR_DISCOVERY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -47,6 +53,202 @@ def api_data(funct): return _wrapper +@bind_hass +async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: + """Return add-on info. + + The add-on must be installed. + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + return await hassio.get_addon_info(slug) + + +@api_data +async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: + """Return add-on store info. + + The caller of the function should handle HassioAPIError. + """ + hassio: HassIO = hass.data[DOMAIN] + command = f"/store/addons/{slug}" + return await hassio.send_command(command, method="get") + + +@bind_hass +async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: + """Update Supervisor diagnostics toggle. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + return await hassio.update_diagnostics(diagnostics) + + +@bind_hass +@api_data +async def async_install_addon(hass: HomeAssistant, slug: str) -> dict: + """Install add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/install" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: + """Uninstall add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/uninstall" + return await hassio.send_command(command, timeout=60) + + +@bind_hass +@api_data +async def async_update_addon( + hass: HomeAssistant, + slug: str, + backup: bool = False, +) -> dict: + """Update add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/update" + return await hassio.send_command( + command, + payload={"backup": backup}, + timeout=None, + ) + + +@bind_hass +@api_data +async def async_start_addon(hass: HomeAssistant, slug: str) -> dict: + """Start add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/start" + return await hassio.send_command(command, timeout=60) + + +@bind_hass +@api_data +async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict: + """Restart add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/restart" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict: + """Stop add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/stop" + return await hassio.send_command(command, timeout=60) + + +@bind_hass +@api_data +async def async_set_addon_options( + hass: HomeAssistant, slug: str, options: dict +) -> dict: + """Set add-on options. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/options" + return await hassio.send_command(command, payload=options) + + +@bind_hass +async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None: + """Return discovery data for an add-on.""" + hassio = hass.data[DOMAIN] + data = await hassio.retrieve_discovery_messages() + discovered_addons = data[ATTR_DISCOVERY] + return next((addon for addon in discovered_addons if addon["addon"] == slug), None) + + +@bind_hass +@api_data +async def async_create_backup( + hass: HomeAssistant, payload: dict, partial: bool = False +) -> dict: + """Create a full or partial backup. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + backup_type = "partial" if partial else "full" + command = f"/backups/new/{backup_type}" + return await hassio.send_command(command, payload=payload, timeout=None) + + +@bind_hass +@api_data +async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict: + """Update Home Assistant Operating System. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/os/update" + return await hassio.send_command( + command, + payload={"version": version}, + timeout=None, + ) + + +@bind_hass +@api_data +async def async_update_supervisor(hass: HomeAssistant) -> dict: + """Update Home Assistant Supervisor. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/supervisor/update" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_update_core( + hass: HomeAssistant, version: str | None = None, backup: bool = False +) -> dict: + """Update Home Assistant Core. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/core/update" + return await hassio.send_command( + command, + payload={"version": version, "backup": backup}, + timeout=None, + ) + + class HassIO: """Small API wrapper for Hass.io.""" diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index d8d29f44d68..795d1e325fb 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -27,7 +27,7 @@ async def system_health_info(hass: HomeAssistant): supervisor_info = get_supervisor_info(hass) healthy: bool | dict[str, str] - if supervisor_info.get("healthy"): + if supervisor_info is not None and supervisor_info.get("healthy"): healthy = True else: healthy = { @@ -36,7 +36,7 @@ async def system_health_info(hass: HomeAssistant): } supported: bool | dict[str, str] - if supervisor_info.get("supported"): + if supervisor_info is not None and supervisor_info.get("supported"): supported = True else: supported = { @@ -70,7 +70,7 @@ async def system_health_info(hass: HomeAssistant): information["installed_addons"] = ", ".join( f"{addon['name']} ({addon['version']})" - for addon in supervisor_info.get("addons", []) + for addon in (supervisor_info or {}).get("addons", []) ) return information diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index 68fcf5f343e..cd7aa5c1923 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -1,4 +1,51 @@ { + "issues": { + "unsupported": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + }, + "unsupported_apparmor": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 AppArmor" + }, + "unsupported_cgroup_version": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 CGroup" + }, + "unsupported_dbus": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 D-Bus" + }, + "unsupported_dns_server": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0430" + }, + "unsupported_docker_version": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Docker" + }, + "unsupported_job_conditions": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0417\u0430\u0449\u0438\u0442\u0438\u0442\u0435 \u0441\u0430 \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0438" + }, + "unsupported_network_manager": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 Network Manager" + }, + "unsupported_os": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u041e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430" + }, + "unsupported_os_agent": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 OS-Agent" + }, + "unsupported_software": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d \u0441\u043e\u0444\u0442\u0443\u0435\u0440" + }, + "unsupported_supervisor_version": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Supervisor" + }, + "unsupported_systemd": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441\u044a\u0441 Systemd" + }, + "unsupported_systemd_journal": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441\u044a\u0441 Systemd Journal" + }, + "unsupported_systemd_resolved": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441\u044a\u0441 Systemd-Resolved" + } + }, "system_health": { "info": { "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0430\u0433\u0435\u043d\u0442\u0430", diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 14679301993..9b10bbc55e5 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.", + "description": "El sistema no \u00e9s saludable a causa de {reason}. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 i per saber com solucionar-ho.", "title": "Sistema no saludable - {reason}" }, + "unhealthy_docker": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 Docker no est\u00e0 configurat correctament. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Docker mal configurat" + }, + "unhealthy_privileged": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 no t\u00e9 acc\u00e9s privilegiat a l'execuci\u00f3 de Docker. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Sense privilegis" + }, + "unhealthy_setup": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 no s'ha pogut completar la configuraci\u00f3 correctament. Pot ser degut a diferents motius, clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Configuraci\u00f3 fallida" + }, + "unhealthy_supervisor": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 ha fallat un intent d'actualitzaci\u00f3 del Supervisor a l'\u00faltima versi\u00f3. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Actualitzaci\u00f3 del Supervisor fallida" + }, + "unhealthy_untrusted": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 s'ha detectat codi o imatges no fiables. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Codi no fiable" + }, "unsupported": { - "description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.", + "description": "El sistema no \u00e9s compatible a causa de {reason}. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 i per saber com solucionar-ho.", "title": "Sistema no compatible - {reason}" + }, + "unsupported_apparmor": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 AppArmor no est\u00e0 funcionant correctament i els complements s'executen en un entorn insegur i no protegit. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb AppArmor" + }, + "unsupported_cgroup_version": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 de Docker CGroup incorrecta. Clica l'enlla\u00e7 per con\u00e8ixer la versi\u00f3 correcta i sobre com solucionar-ho.", + "title": "Sistema no compatible - Versi\u00f3 de CGroup" + }, + "unsupported_connectivity_check": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Home Assistant no pot determinar quan hi ha connexi\u00f3 a internet disponible. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Comprovaci\u00f3 de connectivitat desactivada" + }, + "unsupported_content_trust": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Home Assistant no pot verificar que els continguts en execuci\u00f3 s\u00f3n fiables i no han estat modificats per cap atacant. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Comprovaci\u00f3 de continguts fiables desactivada" + }, + "unsupported_dbus": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 D-Bus no est\u00e0 funcionant correctament. Les comunicacions entre el Supervisor i l'amfitri\u00f3 no poden funcionar sense D-Bus. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb D-Bus" + }, + "unsupported_dns_server": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el servidor DNS proporcionat no funciona correctament i la segona opci\u00f3 est\u00e0 desactivada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes el servidor DNS" + }, + "unsupported_docker_configuration": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el 'deamon' de Docker est\u00e0 funcionant de manera inesperada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Docker mal configurat" + }, + "unsupported_docker_version": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 de Docker incorrecta. Clica l'enlla\u00e7 per con\u00e8ixer la versi\u00f3 correcta i sobre com solucionar-ho.", + "title": "Sistema no compatible - Versi\u00f3 de Docker" + }, + "unsupported_job_conditions": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 una o m\u00e9s condicions de treball ('job conditions'), que protegeixen de fallades i errors inesperats, s'han desactivat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Proteccions desactivades" + }, + "unsupported_lxc": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'est\u00e0 executant en una m\u00e0quina virtual LXC. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - LXC detectat" + }, + "unsupported_network_manager": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el gestor de xarxa ('Network Manager') no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb el gestor de xarxa" + }, + "unsupported_os": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el sistema operatiu utilitzat no s'ha provat o no est\u00e0 fet per utilitzar-se amb el Supervisor. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre els sitemes operatius compatibles i com solucionar-ho.", + "title": "Sistema no compatible - Sistema Operatiu" + }, + "unsupported_os_agent": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 OS-Agent no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb OS-Agent" + }, + "unsupported_restart_policy": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 un contenidor Docker t\u00e9 una pol\u00edtica de reinici que podria provocar problemes en l'arrencada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Pol\u00edtica de reinici dels contenidors" + }, + "unsupported_software": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'ha detectat programari extern al sistema Home Assistant. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Programari no compatible" + }, + "unsupported_source_mods": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'ha modificat el codi font del Supervisor. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Modificaci\u00f3 de codi font del Supervisor" + }, + "unsupported_supervisor_version": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 del Supervisor obsoleta i l'actualitzaci\u00f3 autom\u00e0tica est\u00e0 desactivada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Versi\u00f3 del Supervisor" + }, + "unsupported_systemd": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Systemd no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb Systemd" + }, + "unsupported_systemd_journal": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Systemd Journal i/o el servei d'enlla\u00e7 ('gateway') no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Systemd Resolved no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb Systemd-Resolved" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/cs.json b/homeassistant/components/hassio/translations/cs.json index 97f844a8c81..4c2b2e7eafb 100644 --- a/homeassistant/components/hassio/translations/cs.json +++ b/homeassistant/components/hassio/translations/cs.json @@ -1,4 +1,41 @@ { + "issues": { + "unsupported": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n z {reason} . Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee AppArmor nefunguje spr\u00e1vn\u011b a dopl\u0148ky b\u011b\u017e\u00ed nechr\u00e1n\u011bn\u00fdm a nezabezpe\u010den\u00fdm zp\u016fsobem. Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 probl\u00e9my s aplikac\u00ed AppArmor" + }, + "unsupported_cgroup_version": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee se pou\u017e\u00edv\u00e1 nespr\u00e1vn\u00e1 verze Docker CGroup. Pomoc\u00ed odkazu se dozv\u00edte spr\u00e1vnou verzi a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 verze CGroup" + }, + "unsupported_connectivity_check": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee Home Assistant nem\u016f\u017ee ur\u010dit, kdy je dostupn\u00e9 p\u0159ipojen\u00ed k internetu. Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 Kontrola p\u0159ipojen\u00ed zak\u00e1z\u00e1na" + }, + "unsupported_content_trust": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee Home Assistant nem\u016f\u017ee ov\u011b\u0159it, zda je spou\u0161t\u011bn\u00fd obsah d\u016fv\u011bryhodn\u00fd a nebyl \u00fato\u010dn\u00edky upraven. Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 Kontrola d\u016fv\u011bryhodnosti obsahu zak\u00e1z\u00e1na" + }, + "unsupported_dbus": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee D-Bus nefunguje spr\u00e1vn\u011b. Mnoho v\u011bc\u00ed bez toho sel\u017ee, proto\u017ee supervizor nem\u016f\u017ee komunikovat s hostitelem. Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 probl\u00e9my s D-Bus" + }, + "unsupported_dns_server": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee poskytnut\u00fd server DNS nefunguje spr\u00e1vn\u011b a z\u00e1lo\u017en\u00ed mo\u017enost DNS byla zak\u00e1z\u00e1na. Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 probl\u00e9my se serverem DNS" + }, + "unsupported_docker_configuration": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee d\u00e9mon Docker b\u011b\u017e\u00ed neo\u010dek\u00e1van\u00fdm zp\u016fsobem. Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 Docker je \u0161patn\u011b nakonfigurov\u00e1n" + }, + "unsupported_systemd_resolved": { + "description": "Syst\u00e9m nen\u00ed podporov\u00e1n, proto\u017ee Systemd Resolved chyb\u00ed, je neaktivn\u00ed nebo je \u0161patn\u011b nakonfigurov\u00e1n. Pomoc\u00ed odkazu se dozv\u00edte v\u00edce a jak to opravit." + } + }, "system_health": { "info": { "board": "Deska", diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index f25ae73b423..025d63ceacf 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "Das System ist derzeit aufgrund von \u201e{reason}\u201c fehlerhaft. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System - {reason}" + }, + "unhealthy_docker": { + "description": "Das System ist derzeit fehlerhaft, da Docker falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Ungesundes System - Docker falsch konfiguriert" + }, + "unhealthy_privileged": { + "description": "Das System ist derzeit fehlerhaft, da es keinen privilegierten Zugriff auf die Docker-Laufzeit hat. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Nicht privilegiert" + }, + "unhealthy_setup": { + "description": "Das System ist derzeit fehlerhaft, da die Einrichtung nicht abgeschlossen werden konnte. Dies kann mehrere Gr\u00fcnde haben. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Setup fehlgeschlagen" + }, + "unhealthy_supervisor": { + "description": "Das System ist derzeit fehlerhaft, weil ein Versuch, Supervisor auf die neueste Version zu aktualisieren, fehlgeschlagen ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Supervisor-Update fehlgeschlagen" + }, + "unhealthy_untrusted": { + "description": "Das System ist derzeit nicht fehlerfrei, da es nicht vertrauensw\u00fcrdigen Code oder verwendete Images erkannt hat. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Nicht vertrauensw\u00fcrdiger Code" + }, + "unsupported": { + "description": "Das System wird aufgrund von \u201e{reason}\u201c nicht unterst\u00fctzt. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "Das System wird nicht unterst\u00fctzt, da AppArmor nicht ordnungsgem\u00e4\u00df funktioniert und Add-Ons ungesch\u00fctzt und unsicher ausgef\u00fchrt werden. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - AppArmor Probleme" + }, + "unsupported_cgroup_version": { + "description": "Das System wird nicht unterst\u00fctzt, da die falsche Version von Docker CGroup verwendet wird. Verwende den Link, um die richtige Version zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 CGroup Version" + }, + "unsupported_connectivity_check": { + "description": "Das System wird nicht unterst\u00fctzt, weil Home Assistant nicht feststellen kann, wann eine Internetverbindung verf\u00fcgbar ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Konnektivit\u00e4tspr\u00fcfung deaktiviert" + }, + "unsupported_content_trust": { + "description": "Das System wird nicht unterst\u00fctzt, da Home Assistant nicht \u00fcberpr\u00fcfen kann, ob der ausgef\u00fchrte Inhalt vertrauensw\u00fcrdig ist und nicht von Angreifern ge\u00e4ndert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Inhaltsvertrauenspr\u00fcfung deaktiviert" + }, + "unsupported_dbus": { + "description": "System wird nicht unterst\u00fctzt, da D-Bus nicht richtig funktioniert. Viele Dinge schlagen ohne dies fehl, da Supervisor nicht mit dem Host kommunizieren kann. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 D-Bus-Probleme" + }, + "unsupported_dns_server": { + "description": "Das System wird nicht unterst\u00fctzt, da der bereitgestellte DNS-Server nicht ordnungsgem\u00e4\u00df funktioniert und die Fallback DNS Option deaktiviert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - DNS-Server-Probleme" + }, + "unsupported_docker_configuration": { + "description": "Das System wird nicht unterst\u00fctzt, da der Docker-Daemon auf unerwartete Weise ausgef\u00fchrt wird. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Docker falsch konfiguriert" + }, + "unsupported_docker_version": { + "description": "Das System wird nicht unterst\u00fctzt, da die falsche Version von Docker verwendet wird. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Docker-Version" + }, + "unsupported_job_conditions": { + "description": "Das System wird nicht unterst\u00fctzt, da eine oder mehrere Jobbedingungen deaktiviert wurden, die vor unerwarteten Ausf\u00e4llen und Unterbrechungen sch\u00fctzen. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Schutz deaktiviert" + }, + "unsupported_lxc": { + "description": "Das System wird nicht unterst\u00fctzt, da es in einer virtuellen LXC Maschine ausgef\u00fchrt wird. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 LXC erkannt" + }, + "unsupported_network_manager": { + "description": "Das System wird nicht unterst\u00fctzt, weil Network Manager fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Probleme mit Network Manager" + }, + "unsupported_os": { + "description": "Das System wird nicht unterst\u00fctzt, da das verwendete Betriebssystem nicht f\u00fcr die Verwendung mit Supervisor getestet oder gewartet wurde. Verwende den Link, um zu erfahren, welche Betriebssysteme unterst\u00fctzt werden und wie du das Problem beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Betriebssystem" + }, + "unsupported_os_agent": { + "description": "Das System wird nicht unterst\u00fctzt, weil OS-Agent fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Probleme mit OS-Agenten" + }, + "unsupported_restart_policy": { + "description": "Das System wird nicht unterst\u00fctzt, da f\u00fcr einen Docker-Container eine Neustartrichtlinie festgelegt ist, die beim Start Probleme verursachen k\u00f6nnte. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Container-Neustartrichtlinie" + }, + "unsupported_software": { + "description": "Das System wird nicht unterst\u00fctzt, da zus\u00e4tzliche Software au\u00dferhalb des Home Assistant-\u00d6kosystems erkannt wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Nicht unterst\u00fctzte Software" + }, + "unsupported_source_mods": { + "description": "Das System wird nicht unterst\u00fctzt, da der Supervisor-Quellcode ge\u00e4ndert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Modifikation des Supervisor-Quellcodes" + }, + "unsupported_supervisor_version": { + "description": "Das System wird nicht unterst\u00fctzt, da eine veraltete Version von Supervisor verwendet wird und die automatische Aktualisierung deaktiviert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Supervisor-Version" + }, + "unsupported_systemd": { + "description": "System wird nicht unterst\u00fctzt, weil Systemd fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Systemd Probleme" + }, + "unsupported_systemd_journal": { + "description": "Das System wird nicht unterst\u00fctzt, da das Systemd Journal und/oder der Gateway-Dienst fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Systemd Journal-Probleme" + }, + "unsupported_systemd_resolved": { + "description": "Das System wird nicht unterst\u00fctzt, weil Systemd Resolved fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Von Systemd behobene Probleme" + } + }, "system_health": { "info": { "agent_version": "Agent-Version", diff --git a/homeassistant/components/hassio/translations/el.json b/homeassistant/components/hassio/translations/el.json index 9e9b32d7ce3..a5d6ef68293 100644 --- a/homeassistant/components/hassio/translations/el.json +++ b/homeassistant/components/hassio/translations/el.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03c0\u03af \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03cc\u03bd\u03c4\u03bf\u03c2 \u03bc\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03bb\u03cc\u03b3\u03c9 {reason}. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - {reason}" + }, + "unhealthy_docker": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd \u03bc\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf Docker \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039a\u03b1\u03ba\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Docker" + }, + "unhealthy_privileged": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd \u03bc\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c0\u03c1\u03bf\u03bd\u03bf\u03bc\u03b9\u03b1\u03ba\u03ae \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf docker runtime. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039c\u03b7 \u03c0\u03c1\u03bf\u03bd\u03bf\u03bc\u03b9\u03b1\u03ba\u03cc" + }, + "unhealthy_setup": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd \u03bc\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b4\u03b5\u03bd \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5. \u03a5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03bf\u03b9 \u03bb\u03cc\u03b3\u03bf\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03bf\u03c0\u03bf\u03af\u03bf\u03c5\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bc\u03b2\u03b5\u03af \u03b1\u03c5\u03c4\u03cc, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0397 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5" + }, + "unhealthy_supervisor": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd \u03bc\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b7 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Supervisor \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0397 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03cc\u03c0\u03c4\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5" + }, + "unhealthy_untrusted": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd \u03bc\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03bc\u03b7 \u03b1\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03bf \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 \u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2 \u03c3\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03b3\u03b9\u03ad\u03c2 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039c\u03b7 \u03b1\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2" + }, + "unsupported": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bb\u03cc\u03b3\u03c9 {reason}. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - {reason}" + }, + "unsupported_apparmor": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf AppArmor \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03bc\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03c5\u03bc\u03ad\u03bd\u03bf \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03c4\u03c1\u03cc\u03c0\u03bf. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0396\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 AppArmor" + }, + "unsupported_cgroup_version": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Docker CGroup. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c9\u03c3\u03c4\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0388\u03ba\u03b4\u03bf\u03c3\u03b7 CGroup" + }, + "unsupported_connectivity_check": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03b9 \u03c0\u03cc\u03c4\u03b5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03bc\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf \u0394\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2" + }, + "unsupported_content_trust": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 \u039f\u03b9\u03ba\u03af\u03b1\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03b9 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03bf\u03c5 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03bf \u03ba\u03b1\u03b9 \u03cc\u03c4\u03b9 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c4\u03c1\u03bf\u03c0\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03b5\u03b9\u03c3\u03b2\u03bf\u03bb\u03b5\u03af\u03c2. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b5\u03bc\u03c0\u03b9\u03c3\u03c4\u03bf\u03c3\u03cd\u03bd\u03b7\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2" + }, + "unsupported_dbus": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf D-Bus \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c9\u03c3\u03c4\u03ac. \u03a0\u03bf\u03bb\u03bb\u03ac \u03c0\u03c1\u03ac\u03b3\u03bc\u03b1\u03c4\u03b1 \u03b1\u03c0\u03bf\u03c4\u03c5\u03b3\u03c7\u03ac\u03bd\u03bf\u03c5\u03bd \u03c7\u03c9\u03c1\u03af\u03c2 \u03b1\u03c5\u03c4\u03cc, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03bf \u0395\u03c0\u03cc\u03c0\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03ae\u03c3\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0396\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 D-Bus" + }, + "unsupported_dns_server": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 DNS \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c9\u03c3\u03c4\u03ac \u03ba\u03b1\u03b9 \u03b7 \u03b5\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae DNS \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0396\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae DNS" + }, + "unsupported_docker_configuration": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03bf \u03b4\u03b1\u03af\u03bc\u03bf\u03bd\u03b1\u03c2 Docker \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03b1\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u03a4\u03bf Docker \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c9\u03c3\u03c4\u03ac" + }, + "unsupported_docker_version": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Docker. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c9\u03c3\u03c4\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0388\u03ba\u03b4\u03bf\u03c3\u03b7 Docker" + }, + "unsupported_job_conditions": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03bc\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b8\u03ae\u03ba\u03b5\u03c2 \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03bf\u03b9 \u03bf\u03c0\u03bf\u03af\u03b5\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03bf\u03c5\u03bd \u03b1\u03c0\u03cc \u03b1\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03b5\u03c2 \u03b2\u03bb\u03ac\u03b2\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03c3\u03c0\u03b1\u03c3\u03af\u03bc\u03b1\u03c4\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039f\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b5\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2" + }, + "unsupported_lxc": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03ba\u03ae \u03bc\u03b7\u03c7\u03b1\u03bd\u03ae LXC. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 LXC" + }, + "unsupported_network_manager": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf Network Manager \u03bb\u03b5\u03af\u03c0\u03b5\u03b9, \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0396\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + }, + "unsupported_os": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03cc \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bb\u03b5\u03b3\u03c7\u03b8\u03b5\u03af \u03ae \u03c3\u03c5\u03bd\u03c4\u03b7\u03c1\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf Supervisor. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03ac \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03cc \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1" + }, + "unsupported_os_agent": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf OS-Agent \u03bb\u03b5\u03af\u03c0\u03b5\u03b9, \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0396\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 OS-Agent" + }, + "unsupported_restart_policy": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03ad\u03bd\u03b1 \u03ba\u03bf\u03bd\u03c4\u03ad\u03b9\u03bd\u03b5\u03c1 Docker \u03ad\u03c7\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c3\u03cd\u03bd\u03bf\u03bb\u03bf \u03c0\u03bf\u03bb\u03b9\u03c4\u03b9\u03ba\u03ae\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03ba\u03b1\u03bb\u03ad\u03c3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u03a0\u03bf\u03bb\u03b9\u03c4\u03b9\u03ba\u03ae \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2 \u03ba\u03bf\u03bd\u03c4\u03ad\u03b9\u03bd\u03b5\u03c1" + }, + "unsupported_software": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03bf\u03b9\u03ba\u03bf\u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c4\u03bf\u03c5 Home Assistant. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc" + }, + "unsupported_source_mods": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03bf \u03c0\u03b7\u03b3\u03b1\u03af\u03bf\u03c2 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03c0\u03cc\u03c0\u03c4\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03c1\u03bf\u03c0\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u03a4\u03c1\u03bf\u03c0\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c0\u03b7\u03b3\u03ae\u03c2 \u03b5\u03c0\u03cc\u03c0\u03c4\u03b7" + }, + "unsupported_supervisor_version": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03bb\u03b9\u03ac \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Supervisor \u03ba\u03b1\u03b9 \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0388\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03c0\u03cc\u03c0\u03c4\u03b7" + }, + "unsupported_systemd": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf Systemd \u03bb\u03b5\u03af\u03c0\u03b5\u03b9, \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0396\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 Systemd" + }, + "unsupported_systemd_journal": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf Systemd Journal \u03ae/\u03ba\u03b1\u03b9 \u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03c0\u03cd\u03bb\u03b7\u03c2 \u03bb\u03b5\u03af\u03c0\u03bf\u03c5\u03bd, \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03ac \u03ae \u03ad\u03c7\u03bf\u03c5\u03bd \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0396\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03c4\u03bf Systemd Resolved \u03bb\u03b5\u03af\u03c0\u03b5\u03b9, \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03ac\u03b8\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c4\u03bf \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 - \u0395\u03c0\u03b9\u03bb\u03cd\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1" + } + }, "system_health": { "info": { "agent_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 Agent", diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 243467b9f22..cdfe7f17a44 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -13,7 +13,7 @@ "title": "Unhealthy system - Not privileged" }, "unhealthy_setup": { - "description": "System is currently because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.", + "description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.", "title": "Unhealthy system - Setup failed" }, "unhealthy_supervisor": { diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index f2aef9d7214..202a362fbbc 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "Actualmente el sistema no est\u00e1 en buen estado debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que est\u00e1 mal y c\u00f3mo solucionarlo.", + "description": "Actualmente el sistema no est\u00e1 en buen estado debido a {reason}. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", "title": "Sistema en mal estado: {reason}" }, + "unhealthy_docker": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque Docker est\u00e1 configurado incorrectamente. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado - Docker mal configurado" + }, + "unhealthy_privileged": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque no tiene acceso privilegiado al runtime de Docker. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado - No privilegiado" + }, + "unhealthy_setup": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque la configuraci\u00f3n no se complet\u00f3. Hay varias razones por las que esto puede ocurrir, utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado: La configuraci\u00f3n fall\u00f3" + }, + "unhealthy_supervisor": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque fall\u00f3 un intento de actualizar Supervisor a la \u00faltima versi\u00f3n. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado: La actualizaci\u00f3n del supervisor fall\u00f3" + }, + "unhealthy_untrusted": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque ha detectado c\u00f3digo o im\u00e1genes en uso que no son de confianza. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no saludable: C\u00f3digo no confiable" + }, "unsupported": { - "description": "El sistema no es compatible debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que esto significa y c\u00f3mo volver a un sistema compatible.", + "description": "El sistema no es compatible debido a {reason}. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", "title": "Sistema no compatible: {reason}" + }, + "unsupported_apparmor": { + "description": "El sistema no es compatible porque AppArmor no funciona correctamente y los complementos se ejecutan sin protecci\u00f3n ni seguridad. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con AppArmor" + }, + "unsupported_cgroup_version": { + "description": "El sistema no es compatible porque se est\u00e1 utilizando una versi\u00f3n incorrecta de Docker CGroup. Utiliza el enlace para conocer la versi\u00f3n correcta y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: versi\u00f3n CGroup" + }, + "unsupported_connectivity_check": { + "description": "El sistema no es compatible porque Home Assistant no puede determinar cu\u00e1ndo hay una conexi\u00f3n a Internet disponible. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Verificaci\u00f3n de conectividad deshabilitada" + }, + "unsupported_content_trust": { + "description": "El sistema no es compatible porque Home Assistant no puede verificar que el contenido que se est\u00e1 ejecutando sea confiable y que los atacantes no lo hayan modificado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Verificaci\u00f3n de confianza de contenido deshabilitada" + }, + "unsupported_dbus": { + "description": "El sistema no es compatible porque D-Bus no funciona correctamente. Muchas cosas fallan sin esto, ya que Supervisor no puede comunicarse con el host. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con D-Bus" + }, + "unsupported_dns_server": { + "description": "El sistema no es compatible porque el servidor DNS proporcionado no funciona correctamente y la opci\u00f3n de DNS de respaldo se ha deshabilitado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con el servidor DNS" + }, + "unsupported_docker_configuration": { + "description": "El sistema no es compatible porque el demonio de Docker se est\u00e1 ejecutando de forma inesperada. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Docker mal configurado" + }, + "unsupported_docker_version": { + "description": "El sistema no es compatible porque se est\u00e1 utilizando una versi\u00f3n incorrecta de Docker. Utiliza el enlace para conocer la versi\u00f3n correcta y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Versi\u00f3n de Docker" + }, + "unsupported_job_conditions": { + "description": "El sistema no es compatible porque se han deshabilitado una o m\u00e1s condiciones de trabajo que protegen contra fallos y roturas inesperadas. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Protecciones deshabilitadas" + }, + "unsupported_lxc": { + "description": "El sistema no es compatible porque se est\u00e1 ejecutando en una m\u00e1quina virtual LXC. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - LXC detectado" + }, + "unsupported_network_manager": { + "description": "El sistema no es compatible porque falta Network Manager, est\u00e1 inactivo o mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Network Manager" + }, + "unsupported_os": { + "description": "El sistema no es compatible porque el sistema operativo en uso no se ha probado ni mantenido para su uso con Supervisor. Utiliza el enlace para saber qu\u00e9 sistemas operativos son compatibles y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Sistema operativo" + }, + "unsupported_os_agent": { + "description": "El sistema no es compatible porque OS-Agent falta, est\u00e1 inactivo o mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con OS-Agent" + }, + "unsupported_restart_policy": { + "description": "El sistema no es compatible porque un contenedor Docker tiene un conjunto de pol\u00edticas de reinicio que podr\u00eda causar problemas en el inicio. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Pol\u00edtica de reinicio del contenedor" + }, + "unsupported_software": { + "description": "El sistema no es compatible porque se detect\u00f3 software adicional fuera del ecosistema de Home Assistant. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Software no compatible" + }, + "unsupported_source_mods": { + "description": "El sistema no es compatible porque se modific\u00f3 el c\u00f3digo fuente de Supervisor. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Modificaciones de la fuente del supervisor" + }, + "unsupported_supervisor_version": { + "description": "El sistema no es compatible porque se est\u00e1 utilizando una versi\u00f3n obsoleta de Supervisor y se ha desactivado la actualizaci\u00f3n autom\u00e1tica. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Versi\u00f3n de supervisor" + }, + "unsupported_systemd": { + "description": "El sistema no es compatible porque falta Systemd, est\u00e1 inactivo o est\u00e1 mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Systemd" + }, + "unsupported_systemd_journal": { + "description": "El sistema no es compatible porque Systemd Journal y/o el servicio de puerta de enlace faltan, est\u00e1n inactivos o mal configurados. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "El sistema no es compatible porque falta Systemd Resolved, est\u00e1 inactivo o est\u00e1 mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Systemd-Resolved" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index ea0f78c0c57..d0acf51857c 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "S\u00fcsteem ei ole praegu korras '{reason}' t\u00f5ttu. Kasuta linki, et saada rohkem teavet selle kohta, mis on valesti ja kuidas seda parandada.", + "description": "S\u00fcsteem ei ole praegu korras {reason} t\u00f5ttu. Kasuta linki, et saada rohkem teavet ja kuidas seda parandada.", "title": "Vigane s\u00fcsteem \u2013 {reason}" }, + "unhealthy_docker": { + "description": "S\u00fcsteem on praegu ebatervislik, sest Docker on valesti konfigureeritud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Ebatervislik s\u00fcsteem \u2013 Docker on valesti konfigureeritud" + }, + "unhealthy_privileged": { + "description": "S\u00fcsteem on praegu ebatervislik, sest tal puudub privilegeeritud juurdep\u00e4\u00e4s dokkerite k\u00e4ivituskoodile. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - \u00f5igused puuduvad" + }, + "unhealthy_setup": { + "description": "S\u00fcsteem on hetkel ebatervislik, sest seadistamist ei \u00f5nnestunud l\u00f5pule viia. Sellel v\u00f5ib olla mitu p\u00f5hjust, kasuta linki, et saada rohkem teavet ja teada, kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - seadistamine eba\u00f5nnestus" + }, + "unhealthy_supervisor": { + "description": "S\u00fcsteem on hetkel ebatervislik, sest Supervisori uuendamise katse viimasele versioonile eba\u00f5nnestus. Kasuta linki, et teada saada rohkem ja kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - Supervisori uuendamine eba\u00f5nnestus" + }, + "unhealthy_untrusted": { + "description": "S\u00fcsteem on hetkel ebatervislik, sest on tuvastanud kasutuses oleva ebausaldusv\u00e4\u00e4rse koodi v\u00f5i kujutiste kasutamise. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - ebausaldusv\u00e4\u00e4rne kood" + }, "unsupported": { - "description": "S\u00fcsteemi ei toetata '{reason}' t\u00f5ttu. Kasuta linki, et saada lisateavet selle kohta, mida see t\u00e4hendab ja kuidas toetatud s\u00fcsteemi naasta.", + "description": "S\u00fcsteemi ei toetata {reason} t\u00f5ttu. Kasuta linki, et saada lisateavet ja kuidas seda parandada.", "title": "Toetamata s\u00fcsteem \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "S\u00fcsteem ei ole toetatud, sest AppArmor t\u00f6\u00f6tab valesti ja lisad t\u00f6\u00f6tavad kaitsmata ja ebaturvaliselt. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 AppArmori probleemid" + }, + "unsupported_cgroup_version": { + "description": "S\u00fcsteem ei ole toetatud, sest kasutusel on vale Docker CGroupi versioon. Kasuta linki, et teada saada, milline on \u00f5ige versioon ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 CGroupi versioon" + }, + "unsupported_connectivity_check": { + "description": "S\u00fcsteem ei ole toetatud, sest Home Assistant ei suuda kindlaks teha, millal interneti\u00fchendus on saadaval. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "S\u00fcsteemi ei toetata \u2013 \u00fchenduvuse kontroll on keelatud" + }, + "unsupported_content_trust": { + "description": "S\u00fcsteemi ei toetata, kuna Home Assistant ei saa kontrollida, kas k\u00e4itatav sisu on usaldusv\u00e4\u00e4rne ja seda pole r\u00fcndajad muutnud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 sisu usaldusv\u00e4\u00e4rsuse kontroll on keelatud" + }, + "unsupported_dbus": { + "description": "S\u00fcsteem ei ole toetatud, sest D-Bus t\u00f6\u00f6tab valesti. Paljud asjad ei \u00f5nnestu ilma selleta, sest Supervisor ei saa suhelda hostiga. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem - D-Bus probleemid" + }, + "unsupported_dns_server": { + "description": "S\u00fcsteem ei ole toetatud, sest pakutav DNS-server ei t\u00f6\u00f6ta \u00f5igesti ja varu-DNS-variant on v\u00e4lja l\u00fclitatud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 DNS-serveri probleemid" + }, + "unsupported_docker_configuration": { + "description": "S\u00fcsteem ei ole toetatud, sest Dockeri deemon t\u00f6\u00f6tab ootamatul viisil. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 Docker on valesti konfigureeritud" + }, + "unsupported_docker_version": { + "description": "S\u00fcsteemi ei toetata kuna kasutusel on vale Dockeri versioon. Kasuta linki, et saada teavet \u00f5ige versiooni ja selle parandamise kohta.", + "title": "Toetamata s\u00fcsteem \u2013 Dockeri versioon" + }, + "unsupported_job_conditions": { + "description": "S\u00fcsteemi ei toetata kuna \u00fcks v\u00f5i mitu t\u00f6\u00f6tingimust on blokeeritud, mis kaitsevad ootamatute rikete ja purunemiste eest. Kasuta linki, et saada lisateavet ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem - kaitsed v\u00e4lja l\u00fclitatud" + }, + "unsupported_lxc": { + "description": "S\u00fcsteemi ei toetata, kuna seda k\u00e4itatakse LXC virtuaalmasinas. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 tuvastati LXC" + }, + "unsupported_network_manager": { + "description": "S\u00fcsteemi ei toetata, kuna Network Manager puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 v\u00f5rguhalduri probleemid" + }, + "unsupported_os": { + "description": "S\u00fcsteem ei ole toetatud, sest kasutatavat operatsioonis\u00fcsteemi ei ole testitud ega hooldatud Supervisoriga kasutamiseks. Kasuta linki, milliseid operatsioonis\u00fcsteeme toetatakse ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 operatsioonis\u00fcsteem" + }, + "unsupported_os_agent": { + "description": "S\u00fcsteem ei ole toetatud, sest OS-Agent puudub, on mitteaktiivne v\u00f5i valesti konfigureeritud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 OS-agendi probleemid" + }, + "unsupported_restart_policy": { + "description": "S\u00fcsteem ei ole toetatud, sest Dockeri konteinerile on m\u00e4\u00e4ratud taask\u00e4ivitamise poliitika, mis v\u00f5ib k\u00e4ivitamisel probleeme tekitada. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 konteineri taask\u00e4ivitamise reegel" + }, + "unsupported_software": { + "description": "S\u00fcsteem ei ole toetatud, sest on tuvastatud lisatarkvara v\u00e4ljaspool Home Assistant'i \u00f6kos\u00fcsteemi. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 toetamata tarkvara" + }, + "unsupported_source_mods": { + "description": "S\u00fcsteem ei ole toetatud, sest Supervisori l\u00e4htekoodi on muudetud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 Supervisori allika muudatused" + }, + "unsupported_supervisor_version": { + "description": "S\u00fcsteem ei ole toetatud, sest kasutusel on vananenud Supervisori versioon ja automaatne uuendamine on v\u00e4lja l\u00fclitatud. Kasuta linki, et saada rohkem teavet ja teada, kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem - Supervisori versioon" + }, + "unsupported_systemd": { + "description": "S\u00fcsteemi ei toetata kuna Systemd puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem - Systemd probleemid" + }, + "unsupported_systemd_journal": { + "description": "S\u00fcsteemi ei toetata kuna Systemd Journal ja/v\u00f5i l\u00fc\u00fcsiteenus puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 Systemd Journali probleemid" + }, + "unsupported_systemd_resolved": { + "description": "S\u00fcsteemi ei toetata kuna Systemd Resolved puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 Systemd lahendatud probleemid" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json index 9a042b97e57..6f072495c80 100644 --- a/homeassistant/components/hassio/translations/fr.json +++ b/homeassistant/components/hassio/translations/fr.json @@ -1,4 +1,81 @@ { + "issues": { + "unhealthy": { + "title": "Syst\u00e8me d\u00e9fectueux \u2013\u00a0{reason}" + }, + "unhealthy_docker": { + "title": "Syst\u00e8me d\u00e9fectueux \u2013\u00a0Docker mal configur\u00e9" + }, + "unhealthy_privileged": { + "title": "Syst\u00e8me d\u00e9fectueux \u2013\u00a0Non privil\u00e9gi\u00e9" + }, + "unhealthy_setup": { + "title": "Syst\u00e8me d\u00e9fectueux \u2013\u00a0\u00c9chec de l\u2019installation" + }, + "unhealthy_supervisor": { + "title": "Syst\u00e8me d\u00e9fectueux \u2013\u00a0\u00c9chec de la mise \u00e0 jour du superviseur" + }, + "unhealthy_untrusted": { + "title": "Syst\u00e8me d\u00e9fectueux \u2013\u00a0Code non approuv\u00e9" + }, + "unsupported": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0{reason}" + }, + "unsupported_apparmor": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Probl\u00e8mes li\u00e9s \u00e0 AppArmor" + }, + "unsupported_cgroup_version": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Version de CGroup" + }, + "unsupported_connectivity_check": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0V\u00e9rification de connectivit\u00e9 d\u00e9sactiv\u00e9e" + }, + "unsupported_dbus": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Probl\u00e8mes li\u00e9s \u00e0 D-Bus" + }, + "unsupported_dns_server": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Probl\u00e8mes de serveur DNS" + }, + "unsupported_docker_configuration": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Docker mal configur\u00e9" + }, + "unsupported_docker_version": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Version de Docker" + }, + "unsupported_job_conditions": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Protections d\u00e9sactiv\u00e9es" + }, + "unsupported_lxc": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0LXC d\u00e9tect\u00e9" + }, + "unsupported_network_manager": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Probl\u00e8mes li\u00e9s \u00e0 Network Manager" + }, + "unsupported_os": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Syst\u00e8me d\u2019exploitation" + }, + "unsupported_os_agent": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Probl\u00e8mes li\u00e9s \u00e0 OS-Agent" + }, + "unsupported_restart_policy": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0R\u00e8gle de red\u00e9marrage du conteneur" + }, + "unsupported_software": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Logiciel non pris en charge" + }, + "unsupported_source_mods": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Modifications du code source du superviseur" + }, + "unsupported_supervisor_version": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Version du superviseur" + }, + "unsupported_systemd": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Probl\u00e8mes li\u00e9s \u00e0 Systemd" + }, + "unsupported_systemd_resolved": { + "title": "Syst\u00e8me non pris en charge \u2013\u00a0Probl\u00e8mes li\u00e9s \u00e0 Systemd-Resolved" + } + }, "system_health": { "info": { "agent_version": "Version de l'agent", diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json index 8926338221a..13c9cfd949c 100644 --- a/homeassistant/components/hassio/translations/he.json +++ b/homeassistant/components/hassio/translations/he.json @@ -1,6 +1,117 @@ { + "issues": { + "unhealthy": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05db\u05e8\u05d2\u05e2 \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05d1\u05d2\u05dc\u05dc {reason}. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - {reason}" + }, + "unhealthy_docker": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05ea\u05e7\u05d9\u05e0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea Docker \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - Docker \u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9" + }, + "unhealthy_privileged": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d0\u05d9\u05df \u05dc\u05d4 \u05d2\u05d9\u05e9\u05d4 \u05de\u05d5\u05e8\u05e9\u05d9\u05ea \u05dc\u05d6\u05de\u05df \u05d4\u05e8\u05d9\u05e6\u05d4 \u05e9\u05dc docker. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05dc\u05d0 \u05de\u05d9\u05d5\u05d7\u05e1" + }, + "unhealthy_setup": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d4\u05ea\u05e7\u05e0\u05d4 \u05dc\u05d0 \u05d4\u05d5\u05e9\u05dc\u05de\u05d4. \u05d9\u05e9\u05e0\u05df \u05de\u05e1\u05e4\u05e8 \u05e1\u05d9\u05d1\u05d5\u05ea \u05dc\u05db\u05da \u05e9\u05d6\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05d4\u05ea\u05e8\u05d7\u05e9, \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "unhealthy_supervisor": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05de\u05e4\u05e7\u05d7 \u05dc\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d1\u05d9\u05d5\u05ea\u05e8 \u05e0\u05db\u05e9\u05dc. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05e2\u05d3\u05db\u05d5\u05df \u05d4\u05de\u05e4\u05e7\u05d7 \u05e0\u05db\u05e9\u05dc" + }, + "unhealthy_untrusted": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d9\u05d0 \u05d6\u05d9\u05d4\u05ea\u05d4 \u05e7\u05d5\u05d3 \u05d0\u05d5 \u05ea\u05de\u05d5\u05e0\u05d5\u05ea \u05dc\u05d0 \u05de\u05d4\u05d9\u05de\u05e0\u05d9\u05dd \u05d1\u05e9\u05d9\u05de\u05d5\u05e9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05e7\u05d5\u05d3 \u05dc\u05d0 \u05de\u05d4\u05d9\u05de\u05df" + }, + "unsupported": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05e2\u05e7\u05d1 {reason}. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - {reason}" + }, + "unsupported_apparmor": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-AppArmor \u05e4\u05d5\u05e2\u05dc \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9 \u05d5\u05d4\u05e8\u05d7\u05d1\u05d5\u05ea \u05e4\u05d5\u05e2\u05dc\u05d5\u05ea \u05d1\u05e6\u05d5\u05e8\u05d4 \u05dc\u05d0 \u05de\u05d5\u05d2\u05e0\u05ea \u05d5\u05dc\u05d0 \u05de\u05d0\u05d5\u05d1\u05d8\u05d7\u05ea. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea AppArmor" + }, + "unsupported_cgroup_version": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05dc\u05d0 \u05e0\u05db\u05d5\u05e0\u05d4 \u05e9\u05dc Docker CGroup \u05e0\u05de\u05e6\u05d0\u05ea \u05d1\u05e9\u05d9\u05de\u05d5\u05e9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05d0\u05d5\u05d3\u05d5\u05ea \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05e0\u05db\u05d5\u05e0\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d2\u05e8\u05e1\u05ea CGroup" + }, + "unsupported_connectivity_check": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05d9\u05db\u05d5\u05dc \u05dc\u05e7\u05d1\u05d5\u05e2 \u05de\u05ea\u05d9 \u05d7\u05d9\u05d1\u05d5\u05e8 \u05dc\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05d6\u05de\u05d9\u05df. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05d3\u05d9\u05e7\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8\u05d9\u05d5\u05ea \u05de\u05d5\u05e9\u05d1\u05ea\u05ea" + }, + "unsupported_content_trust": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05d9\u05db\u05d5\u05dc \u05dc\u05d0\u05de\u05ea \u05e9\u05d4\u05ea\u05d5\u05db\u05df \u05d4\u05de\u05d5\u05e4\u05e2\u05dc \u05d4\u05d5\u05d0 \u05de\u05d4\u05d9\u05de\u05df \u05d5\u05d0\u05d9\u05e0\u05d5 \u05e9\u05d5\u05e0\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05ea\u05d5\u05e7\u05e4\u05d9\u05dd. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05d3\u05d9\u05e7\u05ea \u05d0\u05de\u05d5\u05df \u05ea\u05d5\u05db\u05df \u05de\u05d5\u05e9\u05d1\u05ea" + }, + "unsupported_dbus": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-D-Bus \u05e4\u05d5\u05e2\u05dc \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d3\u05d1\u05e8\u05d9\u05dd \u05e8\u05d1\u05d9\u05dd \u05e0\u05db\u05e9\u05dc\u05d9\u05dd \u05dc\u05dc\u05d0 \u05d6\u05d4 \u05db\u05d2\u05d5\u05df \u05d4\u05de\u05e4\u05e7\u05d7 \u05d0\u05d9\u05e0\u05d5 \u05d9\u05db\u05d5\u05dc \u05dc\u05ea\u05e7\u05e9\u05e8 \u05e2\u05dd \u05d4\u05de\u05d0\u05e8\u05d7. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea D-Bus" + }, + "unsupported_dns_server": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05e9\u05e8\u05ea \u05d4-DNS \u05e9\u05e1\u05d5\u05e4\u05e7 \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc \u05db\u05e8\u05d0\u05d5\u05d9 \u05d5\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05d4-DNS \u05d4\u05d7\u05d5\u05d6\u05e8\u05ea \u05d4\u05e4\u05db\u05d4 \u05dc\u05dc\u05d0 \u05d6\u05de\u05d9\u05e0\u05d4. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e9\u05e8\u05ea DNS" + }, + "unsupported_docker_configuration": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4-Docker daemon \u05e4\u05d5\u05e2\u05dc \u05d1\u05d0\u05d5\u05e4\u05df \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - Docker \u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9" + }, + "unsupported_docker_version": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05dc\u05d0 \u05e0\u05db\u05d5\u05e0\u05d4 \u05e9\u05dc Docker \u05e0\u05de\u05e6\u05d0\u05ea \u05d1\u05e9\u05d9\u05de\u05d5\u05e9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05de\u05d4\u05d9 \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05e0\u05db\u05d5\u05e0\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d2\u05e8\u05e1\u05ea Docker" + }, + "unsupported_job_conditions": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05ea\u05e0\u05d0\u05d9 \u05e2\u05d1\u05d5\u05d3\u05d4 \u05d0\u05d7\u05d3 \u05d0\u05d5 \u05d9\u05d5\u05ea\u05e8 \u05d4\u05d5\u05e9\u05d1\u05ea\u05d5 \u05d0\u05e9\u05e8 \u05de\u05d2\u05e0\u05d9\u05dd \u05de\u05e4\u05e0\u05d9 \u05db\u05e9\u05dc\u05d9\u05dd \u05d5\u05e9\u05d1\u05e8\u05d9\u05dd \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d9\u05dd. \u05d9\u05e9 \u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d4\u05d4\u05d2\u05e0\u05d5\u05ea \u05de\u05d5\u05e9\u05d1\u05ea\u05d5\u05ea" + }, + "unsupported_lxc": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d9\u05d0 \u05de\u05d5\u05e4\u05e2\u05dc\u05ea \u05d1\u05de\u05d7\u05e9\u05d1 \u05d5\u05d9\u05e8\u05d8\u05d5\u05d0\u05dc\u05d9 LXC. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d6\u05d5\u05d4\u05ea\u05d4 LXC" + }, + "unsupported_network_manager": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05de\u05e0\u05d4\u05dc \u05d4\u05e8\u05e9\u05ea \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05de\u05e0\u05d4\u05dc \u05d4\u05e8\u05e9\u05ea" + }, + "unsupported_os": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d4\u05e4\u05e2\u05dc\u05d4 \u05e9\u05d1\u05e9\u05d9\u05de\u05d5\u05e9 \u05d0\u05d9\u05e0\u05d4 \u05e0\u05d1\u05d3\u05e7\u05ea \u05d0\u05d5 \u05de\u05ea\u05d5\u05d7\u05d6\u05e7\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05e2\u05dd \u05d4\u05de\u05e4\u05e7\u05d7. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05dc\u05d5 \u05de\u05e2\u05e8\u05db\u05d5\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05e0\u05ea\u05de\u05db\u05d5\u05ea \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4" + }, + "unsupported_os_agent": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-OS-Agent \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea OS-Agent" + }, + "unsupported_restart_policy": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05dc Docker \u05e7\u05d5\u05e0\u05d8\u05d9\u05d9\u05e0\u05e8 \u05d9\u05e9 \u05e2\u05e8\u05db\u05ea \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05de\u05d7\u05d3\u05e9 \u05e9\u05e2\u05dc\u05d5\u05dc\u05d4 \u05dc\u05d2\u05e8\u05d5\u05dd \u05dc\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e2\u05ea \u05d4\u05d0\u05ea\u05d7\u05d5\u05dc. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e7\u05d5\u05e0\u05d8\u05d9\u05d9\u05e0\u05e8" + }, + "unsupported_software": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d6\u05d5\u05d4\u05ea\u05d4 \u05ea\u05d5\u05db\u05e0\u05d4 \u05e0\u05d5\u05e1\u05e4\u05ea \u05de\u05d7\u05d5\u05e5 \u05dc\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d0\u05e7\u05d5\u05dc\u05d5\u05d2\u05d9\u05ea \u05e9\u05dc Home Assistant. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05ea\u05d5\u05db\u05e0\u05d4 \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea" + }, + "unsupported_source_mods": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05e7\u05d5\u05d3 \u05d4\u05de\u05e7\u05d5\u05e8 \u05e9\u05dc \u05d4\u05de\u05e4\u05e7\u05d7 \u05d4\u05e9\u05ea\u05e0\u05d4. \u05d9\u05e9 \u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05de\u05e7\u05d5\u05e8 \u05d4\u05de\u05e4\u05e7\u05d7" + }, + "unsupported_supervisor_version": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d2\u05d9\u05e8\u05e1\u05d4 \u05dc\u05d0 \u05de\u05e2\u05d5\u05d3\u05db\u05e0\u05ea \u05e9\u05dc \u05de\u05e4\u05e7\u05d7 \u05e0\u05de\u05e6\u05d0\u05ea \u05d1\u05e9\u05d9\u05de\u05d5\u05e9 \u05d5\u05e2\u05d3\u05db\u05d5\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05d4\u05d5\u05e9\u05d1\u05ea. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d2\u05e8\u05e1\u05ea \u05de\u05e4\u05e7\u05d7" + }, + "unsupported_systemd": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Systemd \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05de\u05e2\u05e8\u05db\u05ea" + }, + "unsupported_systemd_journal": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Systemd Journal \u05d5/\u05d0\u05d5 \u05e9\u05d9\u05e8\u05d5\u05ea \u05d4\u05e9\u05e2\u05e8 \u05d7\u05e1\u05e8\u05d9\u05dd, \u05d0\u05d9\u05e0\u05dd \u05e4\u05e2\u05d9\u05dc\u05d9\u05dd \u05d0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd \u05d1\u05e6\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05e9\u05dc Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Systemd Resolved \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05e9\u05e0\u05e4\u05ea\u05e8\u05d5 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05d4\u05de\u05e2\u05e8\u05db\u05ea" + } + }, "system_health": { "info": { + "agent_version": "\u05d2\u05e8\u05e1\u05ea \u05d4\u05e1\u05d5\u05db\u05df", "board": "\u05dc\u05d5\u05d7", "disk_total": "\u05e1\u05d4\"\u05db \u05d3\u05d9\u05e1\u05e7", "disk_used": "\u05d3\u05d9\u05e1\u05e7 \u05d1\u05e9\u05d9\u05de\u05d5\u05e9", diff --git a/homeassistant/components/hassio/translations/hr.json b/homeassistant/components/hassio/translations/hr.json new file mode 100644 index 00000000000..c028223588b --- /dev/null +++ b/homeassistant/components/hassio/translations/hr.json @@ -0,0 +1,87 @@ +{ + "issues": { + "unhealthy": { + "description": "Sustav je trenutno nezdrav zbog {reason}. Sustav nije podr\u017ean zbog {reason}. Koristite vezu da saznate vi\u0161e i kako to popraviti.", + "title": "Nezdrav sustav - {reason}" + }, + "unhealthy_docker": { + "title": "Nezdrav sustav - Docker je pogre\u0161no konfiguriran" + }, + "unhealthy_privileged": { + "title": "Nezdrav sustav - Nije privilegiran" + }, + "unhealthy_setup": { + "title": "Neispravan sustav - Postavljanje nije uspjelo" + }, + "unhealthy_supervisor": { + "title": "Nezdravi sustav - A\u017euriranje Supervisora nije uspjelo" + }, + "unhealthy_untrusted": { + "title": "Nezdravi sustav - Nepouzdan kod" + }, + "unsupported": { + "description": "Sustav nije podr\u017ean zbog {reason}. Koristite vezu da saznate vi\u0161e i kako to popraviti.", + "title": "Nepodr\u017eani sustav - {reason}" + }, + "unsupported_apparmor": { + "title": "Nepodr\u017eani sustav - Problemi s AppArmorom" + }, + "unsupported_cgroup_version": { + "title": "Nepodr\u017eani sustav - verzija CGroup" + }, + "unsupported_connectivity_check": { + "title": "Nepodr\u017eani sustav \u2013 Provjera povezivosti je onemogu\u0107ena" + }, + "unsupported_content_trust": { + "title": "Nepodr\u017eani sustav - Provjera pouzdanosti sadr\u017eaja onemogu\u0107ena" + }, + "unsupported_dbus": { + "title": "Nepodr\u017eani sustav - D-Bus problemi" + }, + "unsupported_dns_server": { + "title": "Nepodr\u017eani sustav - Problemi s DNS poslu\u017eiteljem" + }, + "unsupported_docker_configuration": { + "title": "Nepodr\u017eani sustav - Docker je pogre\u0161no konfiguriran" + }, + "unsupported_docker_version": { + "title": "Nepodr\u017eani sustav - Docker verzija" + }, + "unsupported_job_conditions": { + "title": "Nepodr\u017eani sustav - Za\u0161tite onemogu\u0107ene" + }, + "unsupported_lxc": { + "title": "Nepodr\u017eani sustav - LXC otkriven" + }, + "unsupported_network_manager": { + "title": "Nepodr\u017eani sustav - Problemi s upraviteljem mre\u017ee" + }, + "unsupported_os": { + "title": "Nepodr\u017eani sustav - Operativni sustav" + }, + "unsupported_os_agent": { + "title": "Nepodr\u017eani sustav - problemi s OS-Agentom" + }, + "unsupported_restart_policy": { + "title": "Nepodr\u017eani sustav - Pravila ponovnog pokretanja kontejnera" + }, + "unsupported_software": { + "title": "Nepodr\u017eani sustav - Nepodr\u017eani softver" + }, + "unsupported_source_mods": { + "title": "Nepodr\u017eani sustav - Izmjene izvornog koda supervizora" + }, + "unsupported_supervisor_version": { + "title": "Nepodr\u017eani sustav - Supervisor verzija" + }, + "unsupported_systemd": { + "title": "Nepodr\u017eani sustav - Systemd problemi" + }, + "unsupported_systemd_journal": { + "title": "Nepodr\u017eani sustav - Systemd Journal problemi" + }, + "unsupported_systemd_resolved": { + "title": "Nepodr\u017eani sustav - Systemd-Resolved problemi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 604a8ae59e6..14f2d995ff6 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "A rendszer jelenleg renellenes \u00e1llapotban van '{reason}' miatt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet is megtudhat arr\u00f3l, hogy mi a probl\u00e9ma, \u00e9s hogyan jav\u00edthatja ki.", + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van a k\u00f6vetkez\u0151 miatt: {reason}. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", "title": "Rendellenes \u00e1llapot \u2013 {reason}" }, + "unhealthy_docker": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert a Docker helytelen\u00fcl van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Rendellenes rendszer \u2013 A Docker rosszul lett konfigur\u00e1lva" + }, + "unhealthy_privileged": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert nem rendelkezik emelt szint\u0171 hozz\u00e1f\u00e9r\u00e9ssel a Docker-futtat\u00f3k\u00f6rnyezethez. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Rendellenes rendszer - Nem privilegiz\u00e1lt" + }, + "unhealthy_setup": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert a telep\u00edt\u00e9s nem fejez\u0151d\u00f6tt be. Ennek sz\u00e1mos oka lehet, haszn\u00e1lja a linket, hogy t\u00f6bbet megtudjon, \u00e9s hogyan jav\u00edthatja ki ezt.", + "title": "Rendellenes rendszer \u2013 A telep\u00edt\u00e9s sikertelen" + }, + "unhealthy_supervisor": { + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van, mert a Supervisor leg\u00fajabb verzi\u00f3ra t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9s\u00e9nek k\u00eds\u00e9rlete sikertelen volt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Rendellenes rendszer \u2013 A Supervisor friss\u00edt\u00e9se nem siker\u00fclt" + }, + "unhealthy_untrusted": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert nem megb\u00edzhat\u00f3 k\u00f3dot vagy haszn\u00e1latban l\u00e9v\u0151 image-et \u00e9szlelt. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Rendellenes rendszer - Nem megb\u00edzhat\u00f3 k\u00f3d" + }, "unsupported": { - "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: '{reason}'. A hivatkoz\u00e1s seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat arr\u00f3l, mit jelent ez, \u00e9s hogyan t\u00e9rhet vissza egy t\u00e1mogatott rendszerhez.", + "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: {reason} . Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "A rendszer nem t\u00e1mogatott, mert az AppArmor helytelen\u00fcl m\u0171k\u00f6dik, \u00e9s a b\u0151v\u00edtm\u00e9nyek nem v\u00e9dett \u00e9s nem biztons\u00e1gos m\u00f3don futnak. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Nem t\u00e1mogatott rendszer \u2013 AppArmor-probl\u00e9m\u00e1k" + }, + "unsupported_cgroup_version": { + "description": "A rendszer nem t\u00e1mogatott, mert a Docker CGroup nem megfelel\u0151 verzi\u00f3ja van haszn\u00e1latban. A link seg\u00edts\u00e9g\u00e9vel megtudhatja a helyes verzi\u00f3t \u00e9s a jav\u00edt\u00e1s m\u00f3dj\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - CGroup verzi\u00f3" + }, + "unsupported_connectivity_check": { + "description": "A rendszer nem t\u00e1mogatott, mert az Otthoni asszisztens nem tudja meghat\u00e1rozni, hogy van-e m\u0171k\u00f6d\u0151 internetkapcsolat. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Nem t\u00e1mogatott rendszer - Csatlakoz\u00e1si ellen\u0151rz\u00e9s letiltva" + }, + "unsupported_content_trust": { + "description": "A rendszer nem t\u00e1mogatott, mivel a Home Assistant nem tudja ellen\u0151rizni, hogy a futtatott tartalom megb\u00edzhat\u00f3 \u00e9s nem t\u00e1mad\u00f3k \u00e1ltal m\u00f3dos\u00edtott. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Tartalom-megb\u00edzhat\u00f3s\u00e1gi ellen\u0151rz\u00e9s letiltva" + }, + "unsupported_dbus": { + "description": "A rendszer nem t\u00e1mogatott, mert a D-Bus hib\u00e1san m\u0171k\u00f6dik. E n\u00e9lk\u00fcl sok minden meghib\u00e1sodik, mivel a Supervisor nem tud kommunik\u00e1lni a rendszerrel. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan lehet ezt kijav\u00edtani.", + "title": "Nem t\u00e1mogatott rendszer - D-Bus probl\u00e9m\u00e1k" + }, + "unsupported_dns_server": { + "description": "A rendszer nem t\u00e1mogatott, mert a megadott DNS-kiszolg\u00e1l\u00f3 nem m\u0171k\u00f6dik megfelel\u0151en, \u00e9s a tartal\u00e9k DNS opci\u00f3t letiltott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - DNS-kiszolg\u00e1l\u00f3 probl\u00e9m\u00e1k" + }, + "unsupported_docker_configuration": { + "description": "A rendszer nem t\u00e1mogatott, mert a Docker d\u00e9mon nem az elv\u00e1rt m\u00f3don fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer \u2013 A Docker helytelen\u00fcl van konfigur\u00e1lva" + }, + "unsupported_docker_version": { + "description": "A rendszer nem t\u00e1mogatott, mert a Docker nem megfelel\u0151 verzi\u00f3ja van haszn\u00e1latban. A link seg\u00edts\u00e9g\u00e9vel megtudhatja a helyes verzi\u00f3t \u00e9s a jav\u00edt\u00e1s m\u00f3dj\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Docker verzi\u00f3" + }, + "unsupported_job_conditions": { + "description": "A rendszer nem t\u00e1mogatott, mert egy vagy t\u00f6bb feladatfelt\u00e9tel le van tiltva, amelyek v\u00e9delmet ny\u00fajtanak a v\u00e1ratlan hib\u00e1kt\u00f3l. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Nem t\u00e1mogatott rendszer \u2013 A v\u00e9delem le van tiltva" + }, + "unsupported_lxc": { + "description": "A rendszer nem t\u00e1mogatott, mert LXC virtu\u00e1lis g\u00e9pben fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - LXC \u00e9szlelve" + }, + "unsupported_network_manager": { + "description": "A rendszer nem t\u00e1mogatott, mert a H\u00e1l\u00f3zatkezel\u0151 hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Network Manager probl\u00e9m\u00e1k" + }, + "unsupported_os": { + "description": "A rendszer nem t\u00e1mogatott, mert a haszn\u00e1lt oper\u00e1ci\u00f3s rendszert nem tesztelt\u00e9k vagy nem tartj\u00e1k karban a Supervisorral val\u00f3 haszn\u00e1latra. Haszn\u00e1lja a linket, hogy mely oper\u00e1ci\u00f3s rendszerek t\u00e1mogatottak \u00e9s hogyan lehet ezt kijav\u00edtani.", + "title": "Nem t\u00e1mogatott rendszer - Oper\u00e1ci\u00f3s rendszer" + }, + "unsupported_os_agent": { + "description": "A rendszer nem t\u00e1mogatott, mert az OS-\u00dcgyn\u00f6k (agent) hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - OS-\u00dcgyn\u00f6k probl\u00e9m\u00e1k" + }, + "unsupported_restart_policy": { + "description": "A rendszer nem t\u00e1mogatott, mivel a Docker kont\u00e9nerben olyan \u00fajraind\u00edt\u00e1si h\u00e1zirend van be\u00e1ll\u00edtva, amely ind\u00edt\u00e1skor probl\u00e9m\u00e1kat okozhat. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Kont\u00e9ner \u00fajraind\u00edt\u00e1si szab\u00e1lyzat" + }, + "unsupported_software": { + "description": "A rendszer nem t\u00e1mogatott, mert a rendszer a Home Assistant \u00f6kosziszt\u00e9m\u00e1n k\u00edv\u00fcli tov\u00e1bbi szoftvereket \u00e9szlelt. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja ezt.", + "title": "Rendellenes rendszer - Nem t\u00e1mogatott szoftver" + }, + "unsupported_source_mods": { + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor forr\u00e1sk\u00f3dj\u00e1t m\u00f3dos\u00edtott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Supervisor forr\u00e1sm\u00f3dos\u00edt\u00e1sok" + }, + "unsupported_supervisor_version": { + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor egy elavult verzi\u00f3ja van haszn\u00e1latban, \u00e9s az automatikus friss\u00edt\u00e9s le van tiltva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Supervisor verzi\u00f3" + }, + "unsupported_systemd": { + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Systemd probl\u00e9m\u00e1k" + }, + "unsupported_systemd_journal": { + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Journal \u00e9s/vagy az \u00e1tj\u00e1r\u00f3 szolg\u00e1ltat\u00e1s hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva . A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Systemd Journal probl\u00e9m\u00e1k" + }, + "unsupported_systemd_resolved": { + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Resolved hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer \u2013 Systemd Resolved probl\u00e9m\u00e1k" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/id.json b/homeassistant/components/hassio/translations/id.json index 250e6e7d4ad..f18ae6f84c6 100644 --- a/homeassistant/components/hassio/translations/id.json +++ b/homeassistant/components/hassio/translations/id.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "Sistem saat ini tidak sehat karena {reason}. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - {reason}" + }, + "unhealthy_docker": { + "description": "Sistem saat ini tidak sehat karena Docker tidak dikonfigurasi dengan benar. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Docker salah dikonfigurasi" + }, + "unhealthy_privileged": { + "description": "Sistem saat ini tidak sehat karena tidak memiliki akses istimewa ke docker runtime. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Tidak memiliki akses istimewa" + }, + "unhealthy_setup": { + "description": "Sistem saat ini tidak sehat karena penyiapan gagal diselesaikan. Ada sejumlah alasan mengapa hal ini bisa terjadi, gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Penyiapan gagal" + }, + "unhealthy_supervisor": { + "description": "Sistem saat ini tidak sehat karena upaya untuk memperbarui Supervisor ke versi terbaru telah gagal. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Pembaruan supervisor gagal" + }, + "unhealthy_untrusted": { + "description": "Sistem saat ini tidak sehat karena telah mendeteksi kode atau gambar yang tidak dipercaya yang digunakan. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Kode tidak tepercaya" + }, + "unsupported": { + "description": "Sistem tidak didukung karena {reason}. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - {reason}" + }, + "unsupported_apparmor": { + "description": "Sistem tidak didukung karena AppArmor tidak berfungsi dengan benar dan add-on berjalan dengan cara yang tidak terlindungi dan tidak aman. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah AppArmor" + }, + "unsupported_cgroup_version": { + "description": "Sistem tidak didukung karena versi Docker CGroup yang digunakan salah. Gunakan tautan untuk mempelajari versi yang benar dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Versi CGroup" + }, + "unsupported_connectivity_check": { + "description": "Sistem tidak didukung karena Home Assistant tidak dapat menentukan kapan koneksi internet tersedia. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Pemeriksaan konektivitas dinonaktifkan" + }, + "unsupported_content_trust": { + "description": "Sistem tidak didukung karena Home Assistant tidak dapat memverifikasi konten yang sedang dijalankan adalah dipercaya dan tidak dimodifikasi oleh penyerang. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Pemeriksaan kepercayaan konten dinonaktifkan" + }, + "unsupported_dbus": { + "description": "Sistem tidak didukung karena D-Bus bekerja secara tidak benar. Banyak hal gagal tanpa D-Bus ini karena Supervisor tidak dapat berkomunikasi dengan host. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah D-Bus" + }, + "unsupported_dns_server": { + "description": "Sistem tidak didukung karena server DNS yang disediakan tidak berfungsi dengan benar dan opsi DNS fallback telah dinonaktifkan. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah server DNS" + }, + "unsupported_docker_configuration": { + "description": "Sistem tidak didukung karena daemon Docker berjalan dengan cara yang tidak terduga. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Docker salah konfigurasi" + }, + "unsupported_docker_version": { + "description": "Sistem tidak didukung karena versi Docker yang salah sedang digunakan. Gunakan tautan untuk mempelajari versi yang benar dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Versi Docker" + }, + "unsupported_job_conditions": { + "description": "Sistem tidak didukung karena satu atau beberapa kondisi pekerjaan telah dinonaktifkan, yang melindungi dari kegagalan dan kerusakan yang tidak terduga. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Perlindungan dinonaktifkan" + }, + "unsupported_lxc": { + "description": "Sistem tidak didukung karena dijalankan di mesin virtual LXC. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - LXC terdeteksi" + }, + "unsupported_network_manager": { + "description": "Sistem tidak didukung karena Network Manager tidak ada, tidak aktif atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Manajer Jaringan" + }, + "unsupported_os": { + "description": "Sistem tidak didukung karena sistem operasi yang digunakan tidak diuji atau dipelihara untuk digunakan dengan Supervisor. Gunakan tautan ke sistem operasi mana yang didukung dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Sistem Operasi" + }, + "unsupported_os_agent": { + "description": "Sistem tidak didukung karena OS-Agent tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah OS-Agent" + }, + "unsupported_restart_policy": { + "description": "Sistem tidak didukung karena kontainer Docker memiliki kebijakan mulai ulang yang ditetapkan, yang dapat menyebabkan masalah saat mulai. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Kebijakan mulai ulang kontainer" + }, + "unsupported_software": { + "description": "Sistem tidak didukung karena perangkat lunak tambahan di luar ekosistem Home Assistant telah terdeteksi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Perangkat lunak tidak didukung" + }, + "unsupported_source_mods": { + "description": "Sistem tidak didukung karena kode sumber Supervisor telah dimodifikasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Modifikasi kode sumber Supervisor" + }, + "unsupported_supervisor_version": { + "description": "Sistem tidak didukung karena versi Supervisor yang kedaluwarsa sedang digunakan dan pembaruan otomatis telah dinonaktifkan. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Versi Supervisor" + }, + "unsupported_systemd": { + "description": "Sistem tidak didukung karena Systemd tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Systemd" + }, + "unsupported_systemd_journal": { + "description": "Sistem tidak didukung karena Jurnal Systemd dan/atau layanan gateway tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Journal Systemd" + }, + "unsupported_systemd_resolved": { + "description": "Sistem tidak didukung karena Systemd Resolved tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Systemd-Resolved" + } + }, "system_health": { "info": { "agent_version": "Versi Agen", diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 3dc55d0f525..20460b1d4dc 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "Il sistema non \u00e8 attualmente integro a causa di {reason}. Usa il collegamento per saperne di pi\u00f9 e come risolvere il problema.", + "title": "Sistema non pi\u00f9 integro - {reason}" + }, + "unhealthy_docker": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 Docker \u00e8 configurato in modo errato. Usa il link per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Docker configurato in modo errato" + }, + "unhealthy_privileged": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 non ha accesso privilegiato al runtime docker. Usa il link per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Non privilegiato" + }, + "unhealthy_setup": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 l'installazione non \u00e8 stata completata. Ci sono una serie di ragioni per cui ci\u00f2 pu\u00f2 verificarsi, usa il link per saperne di pi\u00f9 e come risolverlo.", + "title": "Sistema non integro - Installazione non riuscita" + }, + "unhealthy_supervisor": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 un tentativo di aggiornare Supervisor all'ultima versione non \u00e8 riuscito. Utilizza il collegamento per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Aggiornamento del Supervisor non riuscito" + }, + "unhealthy_untrusted": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 ha rilevato codice o immagini in uso non attendibili. Utilizza il collegamento per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Codice non attendibile" + }, + "unsupported": { + "description": "Il sistema non \u00e8 supportato a causa di {reason}. Utilizzare il collegamento per ulteriori informazioni sul significato e su come tornare a un sistema supportato.", + "title": "Sistema non supportato - {reason}" + }, + "unsupported_apparmor": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 AppArmor non funziona correttamente e i componenti aggiuntivi sono eseguiti in modo non protetto e non sicuro. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con AppArmor" + }, + "unsupported_cgroup_version": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 in uso una versione errata di Docker CGroup. Utilizza il collegamento per conoscere la versione corretta e come risolvere il problema.", + "title": "Sistema non supportato - Versione di CGroup" + }, + "unsupported_connectivity_check": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Home Assistant non \u00e8 in grado di determinare quando \u00e8 disponibile una connessione a Internet. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Controllo connettivit\u00e0 disabilitato" + }, + "unsupported_content_trust": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Home Assistant non \u00e8 in grado di verificare che il contenuto in esecuzione sia attendibile e non modificato da malintenzionati. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Controllo dell'attendibilit\u00e0 del contenuto disabilitato" + }, + "unsupported_dbus": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 D-Bus non funziona correttamente. Molte cose non funzionano senza di esso perch\u00e9 il Supervisor non pu\u00f2 comunicare con l'host. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con D-Bus" + }, + "unsupported_dns_server": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il server DNS fornito non funziona correttamente e l'opzione DNS di fallback \u00e8 stata disattivata. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con il server DNS" + }, + "unsupported_docker_configuration": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il daemon Docker viene eseguito in modo imprevisto. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Docker configurato in modo errato" + }, + "unsupported_docker_version": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 in uso una versione errata di Docker. Utilizza il collegamento per conoscere la versione corretta e come risolvere il problema.", + "title": "Sistema non supportato - Versione Docker" + }, + "unsupported_job_conditions": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 sono state disattivate una o pi\u00f9 condizioni di lavoro che proteggono da guasti e rotture impreviste. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Protezioni disabilitate" + }, + "unsupported_lxc": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 viene eseguito in una macchina virtuale LXC. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - rilevato LXC" + }, + "unsupported_network_manager": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Network Manager \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Network Manager" + }, + "unsupported_os": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il sistema operativo in uso non \u00e8 stato testato o mantenuto per l'uso con Supervisor. Utilizza il link per sapere quali sistemi operativi sono supportati e come risolvere il problema.", + "title": "Sistema non supportato - Sistema operativo" + }, + "unsupported_os_agent": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 OS-Agent \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con OS-Agent" + }, + "unsupported_restart_policy": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 un container Docker ha impostato un criterio di riavvio che potrebbe causare problemi all'avvio. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Criterio di riavvio del Container" + }, + "unsupported_software": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 stato rilevato un software aggiuntivo esterno all'ecosistema Home Assistant. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Software non supportato" + }, + "unsupported_source_mods": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il codice sorgente del Supervisor \u00e8 stato modificato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Modifiche al codice sorgente del Supervisor" + }, + "unsupported_supervisor_version": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 in uso una versione non aggiornata del Supervisor e l'aggiornamento automatico \u00e8 stato disabilitato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Versione Supervisor" + }, + "unsupported_systemd": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Systemd \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Systemd" + }, + "unsupported_systemd_journal": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Systemd Journal e/o il servizio gateway sono mancanti, inattivi o mal configurati. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Systemd Resolved \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Systemd Resolved" + } + }, "system_health": { "info": { "agent_version": "Versione agente", diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index 1fa10a98921..ee5d5328085 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "Systemet er for \u00f8yeblikket usunt p\u00e5 grunn av {reason} . Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system \u2013 {reason}" + }, + "unhealthy_docker": { + "description": "Systemet er for \u00f8yeblikket usunt fordi Docker er feil konfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Docker feilkonfigurert" + }, + "unhealthy_privileged": { + "description": "Systemet er for \u00f8yeblikket usunt fordi det ikke har privilegert tilgang til docker-kj\u00f8retiden. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Ikke privilegert" + }, + "unhealthy_setup": { + "description": "Systemet er for \u00f8yeblikket usunt fordi oppsettet ikke ble fullf\u00f8rt. Det er flere grunner til at dette kan skje. Bruk lenken for \u00e5 l\u00e6re mer og hvordan du kan fikse dette.", + "title": "Usunt system - Konfigurasjonen mislyktes" + }, + "unhealthy_supervisor": { + "description": "Systemet er for \u00f8yeblikket usunt fordi et fors\u00f8k p\u00e5 \u00e5 oppdatere Supervisor til den nyeste versjonen har mislyktes. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Supervisor-oppdatering mislyktes" + }, + "unhealthy_untrusted": { + "description": "Systemet er for \u00f8yeblikket usunt fordi det har oppdaget uklarert kode eller bilder som er i bruk. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Ubetrodd kode" + }, + "unsupported": { + "description": "Systemet st\u00f8ttes ikke p\u00e5 grunn av {reason} . Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Systemet st\u00f8ttes ikke \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "Systemet st\u00f8ttes ikke fordi AppArmor fungerer feil og tillegg kj\u00f8rer p\u00e5 en ubeskyttet og usikker m\u00e5te. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 AppArmor-problemer" + }, + "unsupported_cgroup_version": { + "description": "Systemet st\u00f8ttes ikke fordi feil versjon av Docker CGroup er i bruk. Bruk linken for \u00e5 l\u00e6re riktig versjon og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes - CGroup-versjon" + }, + "unsupported_connectivity_check": { + "description": "Systemet st\u00f8ttes ikke fordi Home Assistant ikke kan fastsl\u00e5 n\u00e5r en Internett-tilkobling er tilgjengelig. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 tilkoblingskontroll er deaktivert" + }, + "unsupported_content_trust": { + "description": "Systemet st\u00f8ttes ikke fordi Home Assistant ikke kan bekrefte at innhold som kj\u00f8res er klarert og ikke endret av angripere. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 kontroll av innhold og klarering er deaktivert" + }, + "unsupported_dbus": { + "description": "Systemet st\u00f8ttes ikke fordi D-Bus fungerer feil. Mange ting feiler uten dette da veileder ikke kan kommunisere med verten. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Ust\u00f8ttet system - D-Bus-problemer" + }, + "unsupported_dns_server": { + "description": "Systemet st\u00f8ttes ikke fordi den angitte DNS-serveren ikke fungerer som den skal og alternativet for reserve-DNS er deaktivert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 DNS-serverproblemer" + }, + "unsupported_docker_configuration": { + "description": "Systemet st\u00f8ttes ikke fordi Docker-demonen kj\u00f8rer p\u00e5 en uventet m\u00e5te. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Docker er feilkonfigurert" + }, + "unsupported_docker_version": { + "description": "Systemet st\u00f8ttes ikke fordi feil versjon av Docker er i bruk. Bruk linken for \u00e5 l\u00e6re riktig versjon og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Docker-versjon" + }, + "unsupported_job_conditions": { + "description": "Systemet st\u00f8ttes ikke fordi en eller flere jobbbetingelser er deaktivert som beskytter mot uventede feil og brudd. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Systemet som ikke st\u00f8ttes \u2013 Beskyttelse er deaktivert" + }, + "unsupported_lxc": { + "description": "Systemet st\u00f8ttes ikke fordi det kj\u00f8res i en virtuell LXC-maskin. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 LXC oppdaget" + }, + "unsupported_network_manager": { + "description": "Systemet st\u00f8ttes ikke fordi Network Manager mangler, er inaktiv eller feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 problemer med Network Manager" + }, + "unsupported_os": { + "description": "Systemet st\u00f8ttes ikke fordi operativsystemet som er i bruk ikke er testet eller vedlikeholdt for bruk med Supervisor. Bruk lenken til hvilke operativsystemer som st\u00f8ttes og hvordan du fikser dette.", + "title": "Ikke-st\u00f8ttet system - Operativsystem" + }, + "unsupported_os_agent": { + "description": "Systemet st\u00f8ttes ikke fordi OS-Agent mangler, er inaktivt eller feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 OS-Agent-problemer" + }, + "unsupported_restart_policy": { + "description": "Systemet st\u00f8ttes ikke fordi en Docker-beholder har et omstartspolicysett som kan for\u00e5rsake problemer ved oppstart. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 policy for omstart av container" + }, + "unsupported_software": { + "description": "Systemet st\u00f8ttes ikke fordi tilleggsprogramvare utenfor Home Assistant-\u00f8kosystemet er oppdaget. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Systemet som ikke st\u00f8ttes \u2013 programvare som ikke st\u00f8ttes" + }, + "unsupported_source_mods": { + "description": "Systemet st\u00f8ttes ikke fordi Supervisor-kildekoden er endret. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 endringer i veilederkilder" + }, + "unsupported_supervisor_version": { + "description": "Systemet st\u00f8ttes ikke fordi en utdatert versjon av Supervisor er i bruk og automatisk oppdatering er deaktivert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes - Supervisor-versjon" + }, + "unsupported_systemd": { + "description": "Systemet st\u00f8ttes ikke fordi Systemd mangler, er inaktivt eller er feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Systemd-problemer" + }, + "unsupported_systemd_journal": { + "description": "Systemet st\u00f8ttes ikke fordi Systemd Journal og/eller gatewaytjenesten mangler, er inaktiv eller feilkonfigurert . Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Systemet som ikke st\u00f8ttes \u2013 Systemd Journal-problemer" + }, + "unsupported_systemd_resolved": { + "description": "Systemet st\u00f8ttes ikke fordi Systemd Resolved mangler, er inaktivt eller feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Systemd-l\u00f8ste problemer" + } + }, "system_health": { "info": { "agent_version": "Agentversjon", diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json index 8850b7066fd..7ee470bc9bd 100644 --- a/homeassistant/components/hassio/translations/pl.json +++ b/homeassistant/components/hassio/translations/pl.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "System jest obecnie \"niezdrowy\" z powodu \u201e{reason}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Niezdrowy system \u2013 {reason}" + }, + "unhealthy_docker": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c Docker jest niepoprawnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 b\u0142\u0119dna konfiguracja Dockera" + }, + "unhealthy_privileged": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c nie ma uprzywilejowanego dost\u0119pu do Docker runtime. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 brak uprzywilejowania" + }, + "unhealthy_setup": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c konfiguracja nie zosta\u0142a uko\u0144czona. Mo\u017ce si\u0119 tak zdarzy\u0107 z wielu powod\u00f3w, skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 konfiguracja nie powiod\u0142a si\u0119" + }, + "unhealthy_supervisor": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c pr\u00f3ba aktualizacji Supervisora do najnowszej wersji nie powiod\u0142a si\u0119. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 aktualizacja Supervisora nie powiod\u0142a si\u0119" + }, + "unhealthy_untrusted": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c wykry\u0142 niezaufany kod lub obrazy w u\u017cyciu. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 niezaufany kod" + }, + "unsupported": { + "description": "System nie jest obs\u0142ugiwany z powodu \u201e{pow\u00f3d}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c AppArmor dzia\u0142a nieprawid\u0142owo, a dodatki dzia\u0142aj\u0105 w spos\u00f3b niezabezpieczony i niezabezpieczony. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z AppArmor" + }, + "unsupported_cgroup_version": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywana jest niew\u0142a\u015bciwa wersja Docker CGroup. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119, jaka jest poprawna wersja i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 wersja CGroup" + }, + "unsupported_connectivity_check": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c Home Assistant nie mo\u017ce okre\u015bli\u0107, kiedy po\u0142\u0105czenie internetowe jest dost\u0119pne. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 sprawdzanie \u0142\u0105czno\u015bci wy\u0142\u0105czone" + }, + "unsupported_content_trust": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c Home Assistant nie mo\u017ce zweryfikowa\u0107, czy uruchamiana zawarto\u015b\u0107 jest zaufana i nie jest modyfikowana przez atakuj\u0105cych. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2013 kontrola zaufania do tre\u015bci wy\u0142\u0105czona" + }, + "unsupported_dbus": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c D-Bus dzia\u0142a nieprawid\u0142owo. Wiele rzeczy zawodzi bez tego, poniewa\u017c Supervisor nie mo\u017ce komunikowa\u0107 si\u0119 z hostem. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z D-Bus" + }, + "unsupported_dns_server": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c podany serwer DNS nie dzia\u0142a poprawnie, a opcja rezerwowego DNS zosta\u0142a wy\u0142\u0105czona. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z serwerem DNS" + }, + "unsupported_docker_configuration": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c Docker darmon dzia\u0142a w nieoczekiwany spos\u00f3b. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 b\u0142\u0119dna konfiguracja Dockera" + }, + "unsupported_docker_version": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywana jest niew\u0142a\u015bciwa wersja Dockera. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119, jaka jest poprawna wersja i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 wersja Dockera" + }, + "unsupported_job_conditions": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c co najmniej jeden warunek pracy zosta\u0142 wy\u0142\u0105czony, co chroni przed nieoczekiwanymi awariami i uszkodzeniami. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 ochrona wy\u0142\u0105czona" + }, + "unsupported_lxc": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c jest uruchomiony na maszynie wirtualnej LXC. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2013 wykryto LXC" + }, + "unsupported_network_manager": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Mened\u017cera Sieci, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Mened\u017cerem Sieci" + }, + "unsupported_os": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywany system operacyjny nie jest przetestowany ani wspierany do u\u017cytku z Supervisorem. Skorzystaj z linku, aby sprawdzi\u0107 obs\u0142ugiwane systemy operacyjne i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 system operacyjny" + }, + "unsupported_os_agent": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje OS-Agent, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system - problemy z OS-Agent" + }, + "unsupported_restart_policy": { + "description": "System jest nieobs\u0142ugiwany, poniewa\u017c kontener Docker ma ustawion\u0105 polityk\u0119 restartu, kt\u00f3ra mo\u017ce powodowa\u0107 problemy podczas uruchamiania. U\u017cyj linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 polityka restartu kontenera" + }, + "unsupported_software": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c wykryto dodatkowe oprogramowanie spoza ekosystemu Home Assistant. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 nieobs\u0142ugiwane oprogramowanie" + }, + "unsupported_source_mods": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c kod \u017ar\u00f3d\u0142owy Supervisora zosta\u0142 zmodyfikowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 modyfikacja kodu \u017ar\u00f3d\u0142owego Supervisora" + }, + "unsupported_supervisor_version": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywana jest nieaktualna wersja Supervisora, a automatyczna aktualizacja zosta\u0142a wy\u0142\u0105czona. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 wersja Supervisora" + }, + "unsupported_systemd": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Systemd, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Systemd" + }, + "unsupported_systemd_journal": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Systemd Journal i/lub us\u0142ugi bramki, jest ona nieaktywna lub b\u0142\u0119dnie skonfigurowana . Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Systemd Resolved, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Systemd Resolved" + } + }, "system_health": { "info": { "agent_version": "Wersja agenta", diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json index 47e0b6df4ae..d185ebd2f5c 100644 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -1,20 +1,120 @@ { "issues": { "unhealthy": { - "description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a '{reason}'. Use o link para saber mais sobre o que est\u00e1 errado e como corrigi-lo.", + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a {reason}. Use o link para saber mais e como corrigir isso.", "title": "Sistema insalubre - {reason}" }, + "unhealthy_docker": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque o Docker est\u00e1 configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o \u00edntegro - Docker mal configurado" + }, + "unhealthy_privileged": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque n\u00e3o tem acesso privilegiado ao tempo de execu\u00e7\u00e3o do docker. Use o link para saber mais e como corrigir isso.", + "title": "Sistema insalubre - N\u00e3o privilegiado" + }, + "unhealthy_setup": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque a instala\u00e7\u00e3o n\u00e3o foi conclu\u00edda. Existem v\u00e1rias raz\u00f5es pelas quais isso pode ocorrer, use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o \u00edntegro - Falha na configura\u00e7\u00e3o" + }, + "unhealthy_supervisor": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque uma tentativa de atualizar o Supervisor para a vers\u00e3o mais recente falhou. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o \u00edntegro - Falha na atualiza\u00e7\u00e3o do supervisor" + }, + "unhealthy_untrusted": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque detectou c\u00f3digo ou imagens n\u00e3o confi\u00e1veis em uso. Use o link para saber mais e como corrigir isso.", + "title": "Sistema insalubre - C\u00f3digo n\u00e3o confi\u00e1vel" + }, "unsupported": { - "description": "O sistema n\u00e3o \u00e9 suportado devido a '{reason}'. Use o link para saber mais sobre o que isso significa e como retornar a um sistema compat\u00edvel.", + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel devido a {reason}. Use o link para saber mais e como corrigir isso.", "title": "Sistema n\u00e3o suportado - {reason}" + }, + "unsupported_apparmor": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o AppArmor est\u00e1 funcionando incorretamente e os complementos est\u00e3o sendo executados de maneira desprotegida e insegura. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas no AppArmor" + }, + "unsupported_cgroup_version": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque a vers\u00e3o errada do Docker CGroup est\u00e1 em uso. Use o link para saber a vers\u00e3o correta e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Vers\u00e3o do CGroup" + }, + "unsupported_connectivity_check": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o Home Assistant n\u00e3o pode determinar quando uma conex\u00e3o com a Internet est\u00e1 dispon\u00edvel. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Verifica\u00e7\u00e3o de conectividade desativada" + }, + "unsupported_content_trust": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o Home Assistant n\u00e3o pode verificar se o conte\u00fado em execu\u00e7\u00e3o \u00e9 confi\u00e1vel e n\u00e3o foi modificado por invasores. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Verifica\u00e7\u00e3o de confian\u00e7a de conte\u00fado desabilitada" + }, + "unsupported_dbus": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o D-Bus est\u00e1 funcionando incorretamente. Muitas coisas falham sem isso, pois o Supervisor n\u00e3o pode se comunicar com o host. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas de D-Bus" + }, + "unsupported_dns_server": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o servidor DNS fornecido n\u00e3o funciona corretamente e a op\u00e7\u00e3o DNS de fallback foi desativada. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas no servidor DNS" + }, + "unsupported_docker_configuration": { + "description": "O sistema n\u00e3o tem suporte porque o daemon do Docker est\u00e1 sendo executado de maneira inesperada. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Docker configurado incorretamente" + }, + "unsupported_docker_version": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque a vers\u00e3o errada do Docker est\u00e1 em uso. Use o link para saber a vers\u00e3o correta e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Vers\u00e3o do Docker" + }, + "unsupported_job_conditions": { + "description": "O sistema n\u00e3o \u00e9 suportado porque uma ou mais condi\u00e7\u00f5es de trabalho foram desativadas, protegendo contra falhas e quebras inesperadas. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Prote\u00e7\u00f5es desativadas" + }, + "unsupported_lxc": { + "description": "O sistema n\u00e3o \u00e9 suportado porque est\u00e1 sendo executado em uma m\u00e1quina virtual LXC. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - LXC detectado" + }, + "unsupported_network_manager": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Network Manager est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas no Network Manager" + }, + "unsupported_os": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o sistema operacional em uso n\u00e3o foi testado ou mantido para uso com o Supervisor. Use o link para quais sistemas operacionais s\u00e3o suportados e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Sistema operacional" + }, + "unsupported_os_agent": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o OS Agent est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas com o OS Agent" + }, + "unsupported_restart_policy": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque um cont\u00eainer do Docker tem uma pol\u00edtica de reinicializa\u00e7\u00e3o definida que pode causar problemas na inicializa\u00e7\u00e3o. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o compat\u00edvel - Pol\u00edtica de reinicializa\u00e7\u00e3o de cont\u00eainer" + }, + "unsupported_software": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque foi detectado software adicional fora do ecossistema do Home Assistant. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Software n\u00e3o suportado" + }, + "unsupported_source_mods": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o c\u00f3digo-fonte do Supervisor foi modificado. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Modifica\u00e7\u00f5es de origem do supervisor" + }, + "unsupported_supervisor_version": { + "description": "O sistema n\u00e3o \u00e9 suportado porque uma vers\u00e3o desatualizada do Supervisor est\u00e1 em uso e a atualiza\u00e7\u00e3o autom\u00e1tica foi desabilitada. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Vers\u00e3o do Supervisor" + }, + "unsupported_systemd": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Systemd est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - problemas no Systemd" + }, + "unsupported_systemd_journal": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Systemd Journal e/ou o servi\u00e7o de gateway est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - problemas do Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Systemd Resolved est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - problemas resolvidos pelo Systemd" } }, "system_health": { "info": { "agent_version": "Vers\u00e3o do Agent", - "board": "Borda", + "board": "Placa", "disk_total": "Total do disco", - "disk_used": "Disco usado", + "disk_used": "Uso do disco", "docker_version": "Vers\u00e3o do Docker", "healthy": "Saud\u00e1vel", "host_os": "Sistema Operacional Host", @@ -22,7 +122,7 @@ "supervisor_api": "API do supervisor", "supervisor_version": "Vers\u00e3o do Supervisor", "supported": "Suportado", - "update_channel": "Atualizar canal", + "update_channel": "Canal de atualiza\u00e7\u00e3o", "version_api": "API de vers\u00e3o" } } diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 0ab366c1775..e41f77b51ef 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", - "title": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 {reason}. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + }, + "unhealthy_docker": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a Docker \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Docker" + }, + "unhealthy_privileged": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0441\u0440\u0435\u0434\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Docker. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u0442 \u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "unhealthy_setup": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\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. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u043e\u0439\u0442\u0438 \u043f\u043e \u0440\u044f\u0434\u0443 \u043f\u0440\u0438\u0447\u0438\u043d, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0441\u0431\u043e\u0439 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435" + }, + "unhealthy_supervisor": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c Supervisor \u0434\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0441\u0431\u043e\u0439 \u043f\u0440\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 Supervisor" + }, + "unhealthy_untrusted": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u043b\u0438 \u043e\u0431\u0440\u0430\u0437\u044b. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434" }, "unsupported": { - "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0438 \u043a\u0430\u043a \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", - "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 {reason}. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 {reason}" + }, + "unsupported_apparmor": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 AppArmor \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e, \u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f \u043d\u0435\u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u044b\u043c \u0438 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 AppArmor" + }, + "unsupported_cgroup_version": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Docker CGroup. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0432\u0435\u0440\u0441\u0438\u044f CGroup" + }, + "unsupported_connectivity_check": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Home Assistant \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043b\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "unsupported_content_trust": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Home Assistant \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043b\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u043c \u0438 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0451\u043d \u0437\u043b\u043e\u0443\u043c\u044b\u0448\u043b\u0435\u043d\u043d\u0438\u043a\u0430\u043c\u0438. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430" + }, + "unsupported_dbus": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 D-Bus \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e. \u041e\u0442 \u044d\u0442\u043e\u0433\u043e Supervisor \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f \u0441 \u0445\u043e\u0441\u0442\u043e\u043c \u0438 \u043c\u043d\u043e\u0433\u043e\u0435 \u043c\u043e\u0436\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 D-Bus" + }, + "unsupported_dns_server": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 DNS-\u0441\u0435\u0440\u0432\u0435\u0440 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e, \u0430 DNS fallback \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c" + }, + "unsupported_docker_configuration": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0441\u043b\u0443\u0436\u0431\u0430 Docker \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u044b\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Docker" + }, + "unsupported_docker_version": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Docker. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0432\u0435\u0440\u0441\u0438\u044f Docker" + }, + "unsupported_job_conditions": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043e\u0434\u043d\u043e \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u043b\u043e\u0432\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0437\u0430\u0449\u0438\u0449\u0430\u044e\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u043e\u0442 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u044b\u0445 \u0441\u0431\u043e\u0435\u0432. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0437\u0430\u0449\u0438\u0442\u0430" + }, + "unsupported_lxc": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u043d\u0430 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0448\u0438\u043d\u0435 LXC. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 LXC" + }, + "unsupported_network_manager": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Network Manager \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 Network Manager" + }, + "unsupported_os": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0430\u0441\u044c \u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0441 Supervisor. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430" + }, + "unsupported_os_agent": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 OS-Agent \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 OS-Agent" + }, + "unsupported_restart_policy": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0435 Docker \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430" + }, + "unsupported_software": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0435 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u0435, \u043d\u0435 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0435 \u0432 \u044d\u043a\u043e\u0441\u0438\u0441\u0442\u0435\u043c\u0443 Home Assistant. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u041f\u041e" + }, + "unsupported_source_mods": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 Supervisor \u0431\u044b\u043b \u0438\u0437\u043c\u0435\u043d\u0451\u043d. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043c\u043e\u0434\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 Supervisor" + }, + "unsupported_supervisor_version": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Supervisor, \u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0432\u0435\u0440\u0441\u0438\u044f Supervisor" + }, + "unsupported_systemd": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Systemd \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 Systemd" + }, + "unsupported_systemd_journal": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0436\u0443\u0440\u043d\u0430\u043b Systemd \u0438/\u0438\u043b\u0438 \u0441\u043b\u0443\u0436\u0431\u0430 \u0448\u043b\u044e\u0437\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0436\u0443\u0440\u043d\u0430\u043b\u043e\u043c Systemd" + }, + "unsupported_systemd_resolved": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Systemd Resolved \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 Systemd-Resolved" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/sk.json b/homeassistant/components/hassio/translations/sk.json new file mode 100644 index 00000000000..c8c1d58b4c9 --- /dev/null +++ b/homeassistant/components/hassio/translations/sk.json @@ -0,0 +1,43 @@ +{ + "issues": { + "unsupported": { + "description": "Syst\u00e9m nie je podporovan\u00fd z {reason}. Ak sa chcete dozvedie\u0165 viac a ako to opravi\u0165, pou\u017eite odkaz.", + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 {reason}" + }, + "unsupported_cgroup_version": { + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 verzia CGroup" + }, + "unsupported_connectivity_check": { + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 Kontrola pripojenia je vypnut\u00e1" + }, + "unsupported_dbus": { + "title": "Nepodporovan\u00fd syst\u00e9m - probl\u00e9my s D-Bus" + }, + "unsupported_dns_server": { + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 probl\u00e9my so serverom DNS" + }, + "unsupported_docker_configuration": { + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 Docker je nespr\u00e1vne nakonfigurovan\u00fd" + }, + "unsupported_docker_version": { + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 verzia Docker" + }, + "unsupported_lxc": { + "title": "Nepodporovan\u00fd syst\u00e9m - zisten\u00e9 LXC" + }, + "unsupported_software": { + "title": "Nepodporovan\u00fd syst\u00e9m \u2013 Nepodporovan\u00fd softv\u00e9r" + } + }, + "system_health": { + "info": { + "agent_version": "Verzia agenta", + "docker_version": "Verzia Dockera", + "host_os": "Hostite\u013esk\u00fd opera\u010dn\u00fd syst\u00e9m", + "installed_addons": "Nain\u0161talovan\u00e9 doplnky", + "supported": "Podporovan\u00e9", + "update_channel": "Aktualizova\u0165 kan\u00e1l", + "version_api": "Verzia API" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json index 5a503e54937..e2df1d8f916 100644 --- a/homeassistant/components/hassio/translations/zh-Hant.json +++ b/homeassistant/components/hassio/translations/zh-Hant.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "\u7531\u65bc {reason}\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - {reason}" + }, + "unhealthy_docker": { + "description": "\u7531\u65bc Docker \u672a\u6b63\u78ba\u8a2d\u5b9a\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - Docker \u8a2d\u5b9a\u932f\u8aa4" + }, + "unhealthy_privileged": { + "description": "\u7531\u65bc docker runtime \u672a\u7372\u5f97\u5b58\u53d6\u6b0a\u9650\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - \u672a\u53d6\u5f97\u6b0a\u9650" + }, + "unhealthy_setup": { + "description": "\u7531\u65bc\u8a2d\u5b9a\u5931\u6557\u7121\u6cd5\u5b8c\u6210\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - \u8a2d\u5b9a\u5931\u6557" + }, + "unhealthy_supervisor": { + "description": "\u7531\u65bc\u8a66\u5716\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Supervisor \u5931\u6557\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - Supervisor \u66f4\u65b0\u5931\u6557" + }, + "unhealthy_untrusted": { + "description": "\u7531\u65bc\u767c\u73fe\u4f7f\u4f7f\u7528\u4e0d\u53d7\u4fe1\u4efb\u7684\u7a0b\u5f0f\u78bc\u6216\u5716\u50cf\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - \u4e0d\u53d7\u4fe1\u4efb\u4e4b\u7a0b\u5f0f\u78bc" + }, + "unsupported": { + "description": "\u7531\u65bc {reason}\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - {reason}" + }, + "unsupported_apparmor": { + "description": "\u7531\u65bc AppArmor \u672a\u6b63\u5e38\u5de5\u4f5c\u3001\u9644\u52a0\u5143\u4ef6\u4ee5\u672a\u53d7\u4fdd\u8b77\u53ca\u4e0d\u5b89\u5168\u65b9\u5f0f\u57f7\u884c\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - AppArmor \u554f\u984c" + }, + "unsupported_cgroup_version": { + "description": "\u7531\u65bc\u4f7f\u7528\u932f\u8aa4\u7248\u672c\u4e4b Docker CGroup\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - CGroup \u7248\u672c" + }, + "unsupported_connectivity_check": { + "description": "\u7531\u65bc Home Assistant \u7121\u6cd5\u78ba\u5b9a\u7db2\u969b\u7db2\u8def\u9023\u7dda\u662f\u5426\u53ef\u7528\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u9023\u7dda\u6aa2\u67e5\u5df2\u95dc\u9589" + }, + "unsupported_content_trust": { + "description": "\u7531\u65bc Home Assistant \u7121\u6cd5\u9a57\u8b49\u57f7\u884c\u7684\u70ba\u4fe1\u4efb\u5167\u5bb9\uff0c\u672a\u53d7\u5230\u653b\u64ca\u8005\u4fee\u6539\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u5167\u5bb9\u4fe1\u4efb\u6aa2\u67e5\u5df2\u95dc\u9589" + }, + "unsupported_dbus": { + "description": "System is unsupported \u7531\u65bc D-Bus \u672a\u6b63\u5e38\u5de5\u4f5c\u3001\u7cfb\u7d71\u4e0d\u5065\u5eb7\u3002\u7f3a\u4e4f\u6b64\u529f\u80fd\u3001Supervisor \u5c07\u7121\u6cd5\u6b63\u5e38\u8207\u4e3b\u6a5f\u9032\u884c\u901a\u8a0a\u3001\u8a31\u591a\u529f\u80fd\u7686\u7121\u6cd5\u6b63\u5e38\u57f7\u884c\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - D-Bus \u554f\u984c" + }, + "unsupported_dns_server": { + "description": "\u7531\u65bc\u63d0\u4f9b\u7684 DNS \u4f3a\u670d\u5668\u672a\u6b63\u5e38\u5de5\u4f5c\u3001fallback DNS \u9078\u9805\u5df2\u7d93\u95dc\u9589\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - DNS \u4f3a\u670d\u5668\u554f\u984c" + }, + "unsupported_docker_configuration": { + "description": "\u7531\u65bc Docker daemon \u6b63\u4ee5\u672a\u9810\u671f\u65b9\u5f0f\u57f7\u884c\u4e2d\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Docker \u8a2d\u5b9a\u932f\u8aa4" + }, + "unsupported_docker_version": { + "description": "System is unsupported \u7531\u65bc\u4f7f\u7528\u932f\u8aa4\u7248\u672c\u4e4b Docker\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Docker \u7248\u672c" + }, + "unsupported_job_conditions": { + "description": "\u7531\u65bc\u4e00\u500b\u6216\u4ee5\u4e0a\u4fdd\u8b77\u672a\u9810\u671f\u5931\u6557\u6216\u640d\u6bc0\u7684\u689d\u4ef6\u5df2\u906d\u5230\u95dc\u9589\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u4fdd\u8b77\u5df2\u95dc\u9589" + }, + "unsupported_lxc": { + "description": "\u7531\u65bc\u7cfb\u7d71\u6b63\u4ee5 LXC \u865b\u64ec\u6a5f\u57f7\u884c\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u5075\u6e2c\u5230 LXC" + }, + "unsupported_network_manager": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528\u7db2\u8def\u7ba1\u7406\u54e1\uff0c\u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u7db2\u8def\u7ba1\u7406\u54e1\u554f\u984c" + }, + "unsupported_os": { + "description": "\u7531\u65bc\u4f5c\u696d\u7cfb\u7d71\u672a\u91dd\u5c0d\u4f7f\u7528 Supervisor \u9032\u884c\u904e\u6e2c\u8a66\u6216\u7dad\u8b77\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u4f5c\u696d\u7cfb\u7d71" + }, + "unsupported_os_agent": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528 OS-Agent \u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - OS-Agent \u554f\u984c" + }, + "unsupported_restart_policy": { + "description": "\u7531\u65bc Docker container \u8a2d\u5b9a\u4e86\u91cd\u555f\u653f\u7b56\uff0c\u53ef\u80fd\u6703\u5c0e\u81f4\u555f\u52d5\u554f\u984c\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Container \u91cd\u555f\u653f\u7b56" + }, + "unsupported_software": { + "description": "\u7531\u65bc\u5075\u6e2c\u5230 Home Assistant \u751f\u614b\u7cfb\u7d71\u4e4b\u5916\u7684\u9644\u52a0\u8edf\u9ad4\u7248\u672c\u904e\u820a\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u4e0d\u652f\u63f4\u8edf\u9ad4" + }, + "unsupported_source_mods": { + "description": "\u7531\u65bc Supervisor \u4f86\u6e90\u78bc\u906d\u5230\u4fee\u6539\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Supervisor \u4f86\u6e90\u4fee\u6539" + }, + "unsupported_supervisor_version": { + "description": "\u7531\u65bc\u6240\u4f7f\u7528\u7684 Supervisor \u7248\u672c\u904e\u820a\u3001\u4e26\u4e14\u5df2\u95dc\u9589\u81ea\u52d5\u66f4\u65b0\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Supervisor \u7248\u672c" + }, + "unsupported_systemd": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528 Systemd \u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u591a\u8a73\u7d30\u8cc7\u8a0a\uff0c\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Systemd \u554f\u984c" + }, + "unsupported_systemd_journal": { + "description": "\u7531\u65bc Systemd \u65e5\u8a8c\u53ca/\u6216\u7f3a\u5c11\u3001\u672a\u555f\u7528\u9598\u9053\u5668\u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002 \u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u591a\u8a73\u7d30\u8cc7\u8a0a\uff0c\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Systemd \u65e5\u8a8c\u554f\u984c" + }, + "unsupported_systemd_resolved": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528 Systemd \u672c\u6a5f\u89e3\u6790\u6216\u8a2d\u5b9a\u932f\u8aa4\u7684\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u591a\u8a73\u7d30\u8cc7\u8a0a\uff0c\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Systemd \u672c\u6a5f\u89e3\u6790\u554f\u984c" + } + }, "system_health": { "info": { "agent_version": "Agent \u7248\u672c", diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index cfe73ff7c40..25019ec6933 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -163,7 +163,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): _LOGGER.warning("Unknown state: %s", device.status) @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: return ( diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 0a5bcdc4d0a..cca8ad2bf4f 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -413,7 +413,7 @@ class HeosMediaPlayer(MediaPlayerEntity): return self._source_manager.source_list @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """State of the player.""" return PLAY_STATE_TO_STATE[self._player.state] diff --git a/homeassistant/components/heos/translations/sk.json b/homeassistant/components/heos/translations/sk.json new file mode 100644 index 00000000000..d57cf8b2883 --- /dev/null +++ b/homeassistant/components/heos/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + }, + "title": "Pripoji\u0165 sa k Heos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index b9ffa3e4baa..57ad77e0654 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -1,38 +1,14 @@ """The HERE Travel Time integration.""" from __future__ import annotations -from datetime import datetime, time, timedelta import logging -import async_timeout -from herepy import NoRouteFoundError, RouteMode, RoutingApi, RoutingResponse -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_MODE, - CONF_UNIT_SYSTEM, - LENGTH_METERS, - LENGTH_MILES, - Platform, -) +from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.location import find_coordinates -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt -from homeassistant.util.unit_conversion import DistanceConverter from .const import ( - ATTR_DESTINATION, - ATTR_DESTINATION_NAME, - ATTR_DISTANCE, - ATTR_DURATION, - ATTR_DURATION_IN_TRAFFIC, - ATTR_ORIGIN, - ATTR_ORIGIN_NAME, CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, @@ -42,24 +18,22 @@ from .const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - DEFAULT_SCAN_INTERVAL, DOMAIN, - IMPERIAL_UNITS, - NO_ROUTE_ERROR_MESSAGE, - TRAFFIC_MODE_ENABLED, - TRAVEL_MODES_VEHICLE, + TRAVEL_MODE_PUBLIC, ) -from .model import HERERoutingData, HERETravelTimeConfig +from .coordinator import ( + HERERoutingDataUpdateCoordinator, + HERETransitDataUpdateCoordinator, +) +from .model import HERETravelTimeConfig PLATFORMS = [Platform.SENSOR] - _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up HERE Travel Time from a config entry.""" api_key = config_entry.data[CONF_API_KEY] - here_client = RoutingApi(api_key) arrival = ( dt.parse_time(config_entry.options[CONF_ARRIVAL_TIME]) @@ -81,17 +55,26 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID), travel_mode=config_entry.data[CONF_MODE], route_mode=config_entry.options[CONF_ROUTE_MODE], - units=config_entry.options[CONF_UNIT_SYSTEM], arrival=arrival, departure=departure, ) - coordinator = HereTravelTimeDataUpdateCoordinator( - hass, - here_client, - here_travel_time_config, - ) - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: + hass.data.setdefault(DOMAIN, {})[ + config_entry.entry_id + ] = HERETransitDataUpdateCoordinator( + hass, + api_key, + here_travel_time_config, + ) + else: + hass.data.setdefault(DOMAIN, {})[ + config_entry.entry_id + ] = HERERoutingDataUpdateCoordinator( + hass, + api_key, + here_travel_time_config, + ) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -106,173 +89,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok - - -class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): - """HERETravelTime DataUpdateCoordinator.""" - - def __init__( - self, - hass: HomeAssistant, - api: RoutingApi, - config: HERETravelTimeConfig, - ) -> None: - """Initialize.""" - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), - ) - self._api = api - self.config = config - - async def _async_update_data(self) -> HERERoutingData | None: - """Get the latest data from the HERE Routing API.""" - try: - async with async_timeout.timeout(10): - return await self.hass.async_add_executor_job(self._update) - except NoRouteFoundError as error: - raise UpdateFailed(NO_ROUTE_ERROR_MESSAGE) from error - - def _update(self) -> HERERoutingData | None: - """Get the latest data from the HERE Routing API.""" - try: - origin, destination, arrival, departure = self._prepare_parameters() - - _LOGGER.debug( - "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", - origin, - destination, - RouteMode[self.config.route_mode], - RouteMode[self.config.travel_mode], - RouteMode[TRAFFIC_MODE_ENABLED], - arrival, - departure, - ) - - response: RoutingResponse = self._api.public_transport_timetable( - origin, - destination, - True, - [ - RouteMode[self.config.route_mode], - RouteMode[self.config.travel_mode], - RouteMode[TRAFFIC_MODE_ENABLED], - ], - arrival=arrival, - departure=departure, - ) - - _LOGGER.debug("Raw response is: %s", response.response) - - attribution: str | None = None - if "sourceAttribution" in response.response: - attribution = build_hass_attribution( - response.response.get("sourceAttribution") - ) - route: list = response.response["route"] - summary: dict = route[0]["summary"] - waypoint: list = route[0]["waypoint"] - distance: float = summary["distance"] - traffic_time: float = summary["baseTime"] - if self.config.travel_mode in TRAVEL_MODES_VEHICLE: - traffic_time = summary["trafficTime"] - if self.config.units == IMPERIAL_UNITS: - # Convert to miles. - distance = DistanceConverter.convert( - distance, LENGTH_METERS, LENGTH_MILES - ) - else: - # Convert to kilometers - distance = distance / 1000 - return HERERoutingData( - { - ATTR_ATTRIBUTION: attribution, - ATTR_DURATION: round(summary["baseTime"] / 60), # type: ignore[misc] - ATTR_DURATION_IN_TRAFFIC: round(traffic_time / 60), - ATTR_DISTANCE: distance, - ATTR_ORIGIN: ",".join(origin), - ATTR_DESTINATION: ",".join(destination), - ATTR_ORIGIN_NAME: waypoint[0]["mappedRoadName"], - ATTR_DESTINATION_NAME: waypoint[1]["mappedRoadName"], - } - ) - except InvalidCoordinatesException as ex: - _LOGGER.error("Could not call HERE api: %s", ex) - return None - - def _prepare_parameters( - self, - ) -> tuple[list[str], list[str], str | None, str | None]: - """Prepare parameters for the HERE api.""" - - def _from_entity_id(entity_id: str) -> list[str]: - coordinates = find_coordinates(self.hass, entity_id) - if coordinates is None: - raise InvalidCoordinatesException( - f"No coordinatnes found for {entity_id}" - ) - try: - here_formatted_coordinates = coordinates.split(",") - vol.Schema(cv.gps(here_formatted_coordinates)) - except (AttributeError, vol.Invalid) as ex: - raise InvalidCoordinatesException( - f"{coordinates} are not valid coordinates" - ) from ex - return here_formatted_coordinates - - # Destination - if self.config.destination_entity_id is not None: - destination = _from_entity_id(self.config.destination_entity_id) - else: - destination = [ - str(self.config.destination_latitude), - str(self.config.destination_longitude), - ] - - # Origin - if self.config.origin_entity_id is not None: - origin = _from_entity_id(self.config.origin_entity_id) - else: - origin = [ - str(self.config.origin_latitude), - str(self.config.origin_longitude), - ] - - # Arrival/Departure - arrival: str | None = None - departure: str | None = None - if self.config.arrival is not None: - arrival = convert_time_to_isodate(self.config.arrival) - if self.config.departure is not None: - departure = convert_time_to_isodate(self.config.departure) - - if arrival is None and departure is None: - departure = "now" - - return (origin, destination, arrival, departure) - - -def build_hass_attribution(source_attribution: dict) -> str | None: - """Build a hass frontend ready string out of the sourceAttribution.""" - if (suppliers := source_attribution.get("supplier")) is not None: - supplier_titles = [] - for supplier in suppliers: - if (title := supplier.get("title")) is not None: - supplier_titles.append(title) - joined_supplier_titles = ",".join(supplier_titles) - return f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." - return None - - -def convert_time_to_isodate(simple_time: time) -> str: - """Take a time like 08:00:00 and combine it with the current date.""" - combined = datetime.combine(dt.start_of_local_day(), simple_time) - if combined < datetime.now(): - combined = combined + timedelta(days=1) - return combined.isoformat() - - -class InvalidCoordinatesException(Exception): - """Coordinates for origin or destination are malformed.""" diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index 38bd1742c91..48a500f17f0 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -4,7 +4,14 @@ from __future__ import annotations import logging from typing import Any -from herepy import HEREError, InvalidCredentialsError, RouteMode, RoutingApi +from here_routing import ( + HERERoutingApi, + HERERoutingError, + HERERoutingUnauthorizedError, + Place, + TransportMode, +) +from here_transit import HERETransitError import voluptuous as vol from homeassistant import config_entries @@ -14,9 +21,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MODE, CONF_NAME, - CONF_UNIT_SYSTEM, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( @@ -24,7 +30,6 @@ from homeassistant.helpers.selector import ( LocationSelector, TimeSelector, ) -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .const import ( CONF_ARRIVAL_TIME, @@ -38,44 +43,41 @@ from .const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, - IMPERIAL_UNITS, - METRIC_UNITS, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_ENABLED, - TRAFFIC_MODES, TRAVEL_MODE_CAR, - TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_PUBLIC, TRAVEL_MODES, - UNITS, ) _LOGGER = logging.getLogger(__name__) +DEFAULT_OPTIONS = { + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_ARRIVAL_TIME: None, + CONF_DEPARTURE_TIME: None, +} -def validate_api_key(api_key: str) -> None: + +async def async_validate_api_key(api_key: str) -> None: """Validate the user input allows us to connect.""" - known_working_origin = [38.9, -77.04833] - known_working_destination = [39.0, -77.1] - RoutingApi(api_key).public_transport_timetable( - known_working_origin, - known_working_destination, - True, - [ - RouteMode[ROUTE_MODE_FASTEST], - RouteMode[TRAVEL_MODE_CAR], - RouteMode[TRAFFIC_MODE_ENABLED], - ], - arrival=None, - departure="now", + known_working_origin = Place(latitude=38.9, longitude=-77.04833) + known_working_destination = Place(latitude=39.0, longitude=-77.1) + + await HERERoutingApi(api_key).route( + origin=known_working_origin, + destination=known_working_destination, + transport_mode=TransportMode.CAR, ) def get_user_step_schema(data: dict[str, Any]) -> vol.Schema: """Get a populated schema or default.""" + travel_mode = data.get(CONF_MODE, TRAVEL_MODE_CAR) + if travel_mode == "publicTransportTimeTable": + travel_mode = TRAVEL_MODE_PUBLIC return vol.Schema( { vol.Optional( @@ -89,20 +91,6 @@ def get_user_step_schema(data: dict[str, Any]) -> vol.Schema: ) -def default_options(hass: HomeAssistant) -> dict[str, str | None]: - """Get the default options.""" - default = { - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_ARRIVAL_TIME: None, - CONF_DEPARTURE_TIME: None, - CONF_UNIT_SYSTEM: METRIC_UNITS, - } - if hass.config.units is US_CUSTOMARY_SYSTEM: - default[CONF_UNIT_SYSTEM] = IMPERIAL_UNITS - return default - - class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for HERE Travel Time.""" @@ -128,12 +116,10 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input = user_input or {} if user_input: try: - await self.hass.async_add_executor_job( - validate_api_key, user_input[CONF_API_KEY] - ) - except InvalidCredentialsError: + await async_validate_api_key(user_input[CONF_API_KEY]) + except HERERoutingUnauthorizedError: errors["base"] = "invalid_auth" - except HEREError as error: + except (HERERoutingError, HERETransitError) as error: _LOGGER.exception("Unexpected exception: %s", error) errors["base"] = "unknown" if not errors: @@ -200,7 +186,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=self._config[CONF_NAME], data=self._config, - options=default_options(self.hass), + options=DEFAULT_OPTIONS, ) schema = vol.Schema( { @@ -229,7 +215,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=self._config[CONF_NAME], data=self._config, - options=default_options(self.hass), + options=DEFAULT_OPTIONS, ) schema = vol.Schema( {vol.Required(CONF_DESTINATION_ENTITY_ID): EntitySelector()} @@ -251,37 +237,19 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): """Manage the HERE Travel Time options.""" if user_input is not None: self._config = user_input - if self.config_entry.data[CONF_MODE] == TRAVEL_MODE_PUBLIC_TIME_TABLE: - return self.async_show_menu( - step_id="time_menu", - menu_options=["departure_time", "arrival_time", "no_time"], - ) return self.async_show_menu( step_id="time_menu", - menu_options=["departure_time", "no_time"], + menu_options=["departure_time", "arrival_time", "no_time"], ) - defaults = default_options(self.hass) schema = vol.Schema( { - vol.Optional( - CONF_TRAFFIC_MODE, - default=self.config_entry.options.get( - CONF_TRAFFIC_MODE, defaults[CONF_TRAFFIC_MODE] - ), - ): vol.In(TRAFFIC_MODES), vol.Optional( CONF_ROUTE_MODE, default=self.config_entry.options.get( - CONF_ROUTE_MODE, defaults[CONF_ROUTE_MODE] + CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE] ), ): vol.In(ROUTE_MODES), - vol.Optional( - CONF_UNIT_SYSTEM, - default=self.config_entry.options.get( - CONF_UNIT_SYSTEM, defaults[CONF_UNIT_SYSTEM] - ), - ): vol.In(UNITS), } ) diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index ea0dc5c136e..300bbf617cf 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -11,7 +11,6 @@ CONF_ORIGIN = "origin" CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" -CONF_TRAFFIC_MODE = "traffic_mode" CONF_ROUTE_MODE = "route_mode" CONF_ARRIVAL = "arrival" CONF_DEPARTURE = "departure" @@ -24,23 +23,17 @@ TRAVEL_MODE_BICYCLE = "bicycle" TRAVEL_MODE_CAR = "car" TRAVEL_MODE_PEDESTRIAN = "pedestrian" TRAVEL_MODE_PUBLIC = "publicTransport" -TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable" TRAVEL_MODE_TRUCK = "truck" TRAVEL_MODES = [ TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, TRAVEL_MODE_PUBLIC, - TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, ] TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] -TRAFFIC_MODE_ENABLED = "traffic_enabled" -TRAFFIC_MODE_DISABLED = "traffic_disabled" -TRAFFIC_MODES = [TRAFFIC_MODE_ENABLED, TRAFFIC_MODE_DISABLED] - ROUTE_MODE_FASTEST = "fastest" ROUTE_MODE_SHORTEST = "shortest" ROUTE_MODES = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] @@ -55,24 +48,14 @@ ICONS = { TRAVEL_MODE_BICYCLE: ICON_BICYCLE, TRAVEL_MODE_PEDESTRIAN: ICON_PEDESTRIAN, TRAVEL_MODE_PUBLIC: ICON_PUBLIC, - TRAVEL_MODE_PUBLIC_TIME_TABLE: ICON_PUBLIC, TRAVEL_MODE_TRUCK: ICON_TRUCK, } -IMPERIAL_UNITS = "imperial" -METRIC_UNITS = "metric" -UNITS = [METRIC_UNITS, IMPERIAL_UNITS] - ATTR_DURATION = "duration" ATTR_DISTANCE = "distance" ATTR_ORIGIN = "origin" ATTR_DESTINATION = "destination" -ATTR_UNIT_SYSTEM = "unit_system" -ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE - ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" ATTR_ORIGIN_NAME = "origin_name" ATTR_DESTINATION_NAME = "destination_name" - -NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py new file mode 100644 index 00000000000..97759510d36 --- /dev/null +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -0,0 +1,279 @@ +"""The HERE Travel Time integration.""" +from __future__ import annotations + +from datetime import datetime, time, timedelta +import logging + +import here_routing +from here_routing import HERERoutingApi, Return, RoutingMode, Spans, TransportMode +import here_transit +from here_transit import HERETransitApi +import voluptuous as vol + +from homeassistant.const import ATTR_ATTRIBUTION, UnitOfLength +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.location import find_coordinates +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt +from homeassistant.util.unit_conversion import DistanceConverter + +from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, + ATTR_DURATION, + ATTR_DURATION_IN_TRAFFIC, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + ROUTE_MODE_FASTEST, +) +from .model import HERETravelTimeConfig, HERETravelTimeData + +_LOGGER = logging.getLogger(__name__) + + +class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): + """here_routing DataUpdateCoordinator.""" + + def __init__( + self, + hass: HomeAssistant, + api_key: str, + config: HERETravelTimeConfig, + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._api = HERERoutingApi(api_key) + self.config = config + + async def _async_update_data(self) -> HERETravelTimeData | None: + """Get the latest data from the HERE Routing API.""" + origin, destination, arrival, departure = prepare_parameters( + self.hass, self.config + ) + + route_mode = ( + RoutingMode.FAST + if self.config.route_mode == ROUTE_MODE_FASTEST + else RoutingMode.SHORT + ) + + _LOGGER.debug( + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, arrival: %s, departure: %s", + origin, + destination, + route_mode, + TransportMode(self.config.travel_mode), + arrival, + departure, + ) + + response = await self._api.route( + transport_mode=TransportMode(self.config.travel_mode), + origin=here_routing.Place(origin[0], origin[1]), + destination=here_routing.Place(destination[0], destination[1]), + routing_mode=route_mode, + arrival_time=arrival, + departure_time=departure, + return_values=[Return.POLYINE, Return.SUMMARY], + spans=[Spans.NAMES], + ) + + _LOGGER.debug("Raw response is: %s", response) + + return self._parse_routing_response(response) + + def _parse_routing_response(self, response) -> HERETravelTimeData: + """Parse the routing response dict to a HERETravelTimeData.""" + section: dict = response["routes"][0]["sections"][0] + summary: dict = section["summary"] + mapped_origin_lat: float = section["departure"]["place"]["location"]["lat"] + mapped_origin_lon: float = section["departure"]["place"]["location"]["lng"] + mapped_destination_lat: float = section["arrival"]["place"]["location"]["lat"] + mapped_destination_lon: float = section["arrival"]["place"]["location"]["lng"] + distance: float = DistanceConverter.convert( + summary["length"], UnitOfLength.METERS, UnitOfLength.KILOMETERS + ) + origin_name: str | None = None + if (names := section["spans"][0].get("names")) is not None: + origin_name = names[0]["value"] + destination_name: str | None = None + if (names := section["spans"][-1].get("names")) is not None: + destination_name = names[0]["value"] + return HERETravelTimeData( + { + ATTR_ATTRIBUTION: None, + ATTR_DURATION: round(summary["baseDuration"] / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(summary["duration"] / 60), + ATTR_DISTANCE: distance, + ATTR_ORIGIN: f"{mapped_origin_lat},{mapped_origin_lon}", + ATTR_DESTINATION: f"{mapped_destination_lat},{mapped_destination_lon}", + ATTR_ORIGIN_NAME: origin_name, + ATTR_DESTINATION_NAME: destination_name, + } + ) + + +class HERETransitDataUpdateCoordinator(DataUpdateCoordinator): + """HERETravelTime DataUpdateCoordinator.""" + + def __init__( + self, + hass: HomeAssistant, + api_key: str, + config: HERETravelTimeConfig, + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._api = HERETransitApi(api_key) + self.config = config + + async def _async_update_data(self) -> HERETravelTimeData | None: + """Get the latest data from the HERE Routing API.""" + origin, destination, arrival, departure = prepare_parameters( + self.hass, self.config + ) + + _LOGGER.debug( + "Requesting transit route for origin: %s, destination: %s, arrival: %s, departure: %s", + origin, + destination, + arrival, + departure, + ) + + response = await self._api.route( + origin=here_transit.Place(latitude=origin[0], longitude=origin[1]), + destination=here_transit.Place( + latitude=destination[0], longitude=destination[1] + ), + arrival_time=arrival, + departure_time=departure, + return_values=[ + here_transit.Return.POLYLINE, + here_transit.Return.TRAVEL_SUMMARY, + ], + ) + + _LOGGER.debug("Raw response is: %s", response) + + return self._parse_transit_response(response) + + def _parse_transit_response(self, response) -> HERETravelTimeData: + """Parse the transit response dict to a HERETravelTimeData.""" + sections: dict = response["routes"][0]["sections"] + attribution: str | None = build_hass_attribution(sections) + mapped_origin_lat: float = sections[0]["departure"]["place"]["location"]["lat"] + mapped_origin_lon: float = sections[0]["departure"]["place"]["location"]["lng"] + mapped_destination_lat: float = sections[-1]["arrival"]["place"]["location"][ + "lat" + ] + mapped_destination_lon: float = sections[-1]["arrival"]["place"]["location"][ + "lng" + ] + distance: float = DistanceConverter.convert( + sum(section["travelSummary"]["length"] for section in sections), + UnitOfLength.METERS, + UnitOfLength.KILOMETERS, + ) + duration: float = sum( + section["travelSummary"]["duration"] for section in sections + ) + return HERETravelTimeData( + { + ATTR_ATTRIBUTION: attribution, + ATTR_DURATION: round(duration / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(duration / 60), + ATTR_DISTANCE: distance, + ATTR_ORIGIN: f"{mapped_origin_lat},{mapped_origin_lon}", + ATTR_DESTINATION: f"{mapped_destination_lat},{mapped_destination_lon}", + ATTR_ORIGIN_NAME: sections[0]["departure"]["place"].get("name"), + ATTR_DESTINATION_NAME: sections[-1]["arrival"]["place"].get("name"), + } + ) + + +def prepare_parameters( + hass: HomeAssistant, + config: HERETravelTimeConfig, +) -> tuple[list[str], list[str], str | None, str | None]: + """Prepare parameters for the HERE api.""" + + def _from_entity_id(entity_id: str) -> list[str]: + coordinates = find_coordinates(hass, entity_id) + if coordinates is None: + raise UpdateFailed(f"No coordinates found for {entity_id}") + if coordinates is entity_id: + raise UpdateFailed(f"Could not find entity {entity_id}") + try: + formatted_coordinates = coordinates.split(",") + vol.Schema(cv.gps(formatted_coordinates)) + except (AttributeError, vol.ExactSequenceInvalid) as ex: + raise UpdateFailed( + f"{entity_id} does not have valid coordinates: {coordinates}" + ) from ex + return formatted_coordinates + + # Destination + if config.destination_entity_id is not None: + destination = _from_entity_id(config.destination_entity_id) + else: + destination = [ + str(config.destination_latitude), + str(config.destination_longitude), + ] + + # Origin + if config.origin_entity_id is not None: + origin = _from_entity_id(config.origin_entity_id) + else: + origin = [ + str(config.origin_latitude), + str(config.origin_longitude), + ] + + # Arrival/Departure + arrival: str | None = None + departure: str | None = None + if config.arrival is not None: + arrival = next_datetime(config.arrival).isoformat() + if config.departure is not None: + departure = next_datetime(config.departure).isoformat() + + return (origin, destination, arrival, departure) + + +def build_hass_attribution(sections: dict) -> str | None: + """Build a hass frontend ready string out of the attributions.""" + relevant_attributions = [] + for section in sections: + if (attributions := section.get("attributions")) is not None: + for attribution in attributions: + if (href := attribution.get("href")) is not None: + relevant_attributions.append(f"{href}") + if (text := attribution.get("text")) is not None: + relevant_attributions.append(text) + if len(relevant_attributions) > 0: + return ",".join(relevant_attributions) + return None + + +def next_datetime(simple_time: time) -> datetime: + """Take a time like 08:00:00 and combine it with the current date.""" + combined = datetime.combine(dt.start_of_local_day(), simple_time) + if combined < datetime.now(): + combined = combined + timedelta(days=1) + return combined diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 68370311254..8efcf29b6b0 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,8 +3,8 @@ "name": "HERE Travel Time", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/here_travel_time", - "requirements": ["herepy==2.0.0"], + "requirements": ["here_routing==0.1.1", "here_transit==1.0.0"], "codeowners": ["@eifinger"], "iot_class": "cloud_polling", - "loggers": ["herepy"] + "loggers": ["here_routing", "here_transit"] } diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index 7310ac24e77..6d8d4826007 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -6,8 +6,8 @@ from datetime import time from typing import TypedDict -class HERERoutingData(TypedDict): - """Routing information calculated from a herepy.RoutingResponse.""" +class HERETravelTimeData(TypedDict): + """Routing information.""" ATTR_ATTRIBUTION: str | None ATTR_DURATION: float @@ -31,6 +31,5 @@ class HERETravelTimeConfig: origin_entity_id: str | None travel_mode: str route_mode: str - units: str arrival: time | None departure: time | None diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 1ee0087eab7..7e1c93bcfb5 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -6,7 +6,8 @@ from datetime import timedelta from typing import Any from homeassistant.components.sensor import ( - SensorEntity, + RestoreSensor, + SensorDeviceClass, SensorEntityDescription, SensorStateClass, ) @@ -17,18 +18,16 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONF_MODE, CONF_NAME, - LENGTH_KILOMETERS, - LENGTH_MILES, TIME_MINUTES, + UnitOfLength, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.start import async_at_start +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import HereTravelTimeDataUpdateCoordinator from .const import ( ATTR_DESTINATION, ATTR_DESTINATION_NAME, @@ -40,8 +39,8 @@ from .const import ( DOMAIN, ICON_CAR, ICONS, - IMPERIAL_UNITS, ) +from .coordinator import HERERoutingDataUpdateCoordinator SCAN_INTERVAL = timedelta(minutes=5) @@ -63,6 +62,14 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TIME_MINUTES, ), + SensorEntityDescription( + name="Distance", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, + ), ) @@ -89,11 +96,10 @@ async def async_setup_entry( ) sensors.append(OriginSensor(entry_id, name, coordinator)) sensors.append(DestinationSensor(entry_id, name, coordinator)) - sensors.append(DistanceSensor(entry_id, name, coordinator)) async_add_entities(sensors) -class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): +class HERETravelTimeSensor(CoordinatorEntity, RestoreSensor): """Representation of a HERE travel time sensor.""" def __init__( @@ -101,7 +107,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): unique_id_prefix: str, name: str, sensor_description: SensorEntityDescription, - coordinator: HereTravelTimeDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -115,21 +121,29 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): ) self._attr_has_entity_name = True + async def _async_restore_state(self) -> None: + """Restore state.""" + if restored_data := await self.async_get_last_sensor_data(): + self._attr_native_value = restored_data.native_value + async def async_added_to_hass(self) -> None: """Wait for start so origin and destination entities can be resolved.""" + await self._async_restore_state() await super().async_added_to_hass() async def _update_at_start(_): await self.async_update() - self.async_on_remove(async_at_start(self.hass, _update_at_start)) + self.async_on_remove(async_at_started(self.hass, _update_at_start)) - @property - def native_value(self) -> str | float | None: - """Return the state of the sensor.""" + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" if self.coordinator.data is not None: - return self.coordinator.data.get(self.entity_description.key) - return None + self._attr_native_value = self.coordinator.data.get( + self.entity_description.key + ) + self.async_write_ha_state() @property def attribution(self) -> str | None: @@ -146,7 +160,7 @@ class OriginSensor(HERETravelTimeSensor): self, unique_id_prefix: str, name: str, - coordinator: HereTravelTimeDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" sensor_description = SensorEntityDescription( @@ -174,7 +188,7 @@ class DestinationSensor(HERETravelTimeSensor): self, unique_id_prefix: str, name: str, - coordinator: HereTravelTimeDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" sensor_description = SensorEntityDescription( @@ -193,29 +207,3 @@ class DestinationSensor(HERETravelTimeSensor): ATTR_LONGITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[1], } return None - - -class DistanceSensor(HERETravelTimeSensor): - """Sensor holding information about the distance.""" - - def __init__( - self, - unique_id_prefix: str, - name: str, - coordinator: HereTravelTimeDataUpdateCoordinator, - ) -> None: - """Initialize the sensor.""" - sensor_description = SensorEntityDescription( - name="Distance", - icon=ICONS.get(coordinator.config.travel_mode, ICON_CAR), - key=ATTR_DISTANCE, - state_class=SensorStateClass.MEASUREMENT, - ) - super().__init__(unique_id_prefix, name, sensor_description, coordinator) - - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit of measurement of the sensor.""" - if self.coordinator.config.units == IMPERIAL_UNITS: - return LENGTH_MILES - return LENGTH_KILOMETERS diff --git a/homeassistant/components/here_travel_time/translations/pl.json b/homeassistant/components/here_travel_time/translations/pl.json index 17b91417007..e409971f3b3 100644 --- a/homeassistant/components/here_travel_time/translations/pl.json +++ b/homeassistant/components/here_travel_time/translations/pl.json @@ -72,7 +72,7 @@ "init": { "data": { "route_mode": "Tryb trasy", - "traffic_mode": "Tryb ruchu", + "traffic_mode": "Tryb nat\u0119\u017cenia ruchu", "unit_system": "System metryczny" } }, diff --git a/homeassistant/components/here_travel_time/translations/sk.json b/homeassistant/components/here_travel_time/translations/sk.json new file mode 100644 index 00000000000..7bb5d95a7e5 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/sk.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "destination_entity_id": { + "title": "Vyberte cie\u013e" + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u010cas pr\u00edchodu" + }, + "title": "V\u00fdber \u010dasu pr\u00edchodu" + }, + "departure_time": { + "data": { + "departure_time": "\u010cas odchodu" + }, + "title": "V\u00fdber \u010dasu odchodu" + }, + "time_menu": { + "menu_options": { + "no_time": "Nekonfigurujte \u010das" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hexaom/__init__.py b/homeassistant/components/hexaom/__init__.py new file mode 100644 index 00000000000..9b46a4f0e1c --- /dev/null +++ b/homeassistant/components/hexaom/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Hexaom Hexaconnect.""" diff --git a/homeassistant/components/hexaom/manifest.json b/homeassistant/components/hexaom/manifest.json new file mode 100644 index 00000000000..738ffdb4fbf --- /dev/null +++ b/homeassistant/components/hexaom/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "hexaom", + "name": "Hexaom Hexaconnect", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index e7f3c49fb08..ef8a18ec3ae 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -46,7 +46,6 @@ DEVICE_CLASS_MAP = { "Motion": BinarySensorDeviceClass.MOTION, "Line Crossing": BinarySensorDeviceClass.MOTION, "Field Detection": BinarySensorDeviceClass.MOTION, - "Video Loss": None, "Tamper Detection": BinarySensorDeviceClass.MOTION, "Shelter Alarm": None, "Disk Full": None, diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 7209ee00024..4b27e37b0f6 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -2,7 +2,7 @@ "domain": "hikvision", "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", - "requirements": ["pyhik==0.3.0"], + "requirements": ["pyhik==0.3.1"], "codeowners": ["@mezz64"], "iot_class": "local_push", "loggers": ["pyhik"] diff --git a/homeassistant/components/hisense_aehw4a1/translations/he.json b/homeassistant/components/hisense_aehw4a1/translations/he.json index 380dbc5d7fc..032c9c9fa17 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/he.json +++ b/homeassistant/components/hisense_aehw4a1/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/sk.json b/homeassistant/components/hisense_aehw4a1/translations/sk.json new file mode 100644 index 00000000000..99798036ffd --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 165ef72d525..f07fe82b50d 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -13,11 +13,7 @@ import voluptuous as vol from homeassistant.components import frontend, websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import ( - get_instance, - history, - websocket_api as recorder_ws, -) +from homeassistant.components.recorder import get_instance, history from homeassistant.components.recorder.filters import ( Filters, sqlalchemy_filter_from_include_exclude_conf, @@ -61,52 +57,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(HistoryPeriodView(filters, use_include_order)) frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box") - websocket_api.async_register_command(hass, ws_get_statistics_during_period) - websocket_api.async_register_command(hass, ws_get_list_statistic_ids) websocket_api.async_register_command(hass, ws_get_history_during_period) return True -@websocket_api.websocket_command( - { - vol.Required("type"): "history/statistics_during_period", - vol.Required("start_time"): str, - vol.Optional("end_time"): str, - vol.Optional("statistic_ids"): [str], - vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), - } -) -@websocket_api.async_response -async def ws_get_statistics_during_period( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Handle statistics websocket command.""" - _LOGGER.warning( - "WS API 'history/statistics_during_period' is deprecated and will be removed in " - "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead" - ) - await recorder_ws.ws_handle_get_statistics_during_period(hass, connection, msg) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "history/list_statistic_ids", - vol.Optional("statistic_type"): vol.Any("sum", "mean"), - } -) -@websocket_api.async_response -async def ws_get_list_statistic_ids( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Fetch a list of available statistic_id.""" - _LOGGER.warning( - "WS API 'history/list_statistic_ids' is deprecated and will be removed in " - "Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead" - ) - await recorder_ws.ws_handle_list_statistic_ids(hass, connection, msg) - - def _ws_get_significant_states( hass: HomeAssistant, msg_id: int, diff --git a/homeassistant/components/hive/translations/bg.json b/homeassistant/components/hive/translations/bg.json index c027da47da5..0484d63a5b5 100644 --- a/homeassistant/components/hive/translations/bg.json +++ b/homeassistant/components/hive/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "no_internet_available": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0441 Hive." diff --git a/homeassistant/components/hive/translations/cs.json b/homeassistant/components/hive/translations/cs.json index 81e2a4b288e..85a259b8992 100644 --- a/homeassistant/components/hive/translations/cs.json +++ b/homeassistant/components/hive/translations/cs.json @@ -17,9 +17,19 @@ "user": { "data": { "password": "Heslo", + "scan_interval": "Interval skenov\u00e1n\u00ed (sekundy)", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Interval skenov\u00e1n\u00ed (sekundy)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/de.json b/homeassistant/components/hive/translations/de.json index e40c7a1adcb..1e1453df99e 100644 --- a/homeassistant/components/hive/translations/de.json +++ b/homeassistant/components/hive/translations/de.json @@ -24,8 +24,8 @@ "data": { "device_name": "Ger\u00e4tename" }, - "description": "Gib deine Hive-Konfiguration ein", - "title": "Hive-Konfiguration." + "description": "Gib deine Hive Konfiguration ein", + "title": "Hive Konfiguration." }, "reauth": { "data": { diff --git a/homeassistant/components/hive/translations/sk.json b/homeassistant/components/hive/translations/sk.json index c2f015fe339..18d0c4e60db 100644 --- a/homeassistant/components/hive/translations/sk.json +++ b/homeassistant/components/hive/translations/sk.json @@ -1,7 +1,57 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown_entry": "Nie je mo\u017en\u00e9 n\u00e1js\u0165 existuj\u00faci z\u00e1znam." + }, + "error": { + "invalid_username": "Nepodarilo sa prihl\u00e1si\u0165 do syst\u00e9mu Hive. Va\u0161a e-mailov\u00e1 adresa nie je rozpoznan\u00e1.", + "no_internet_available": "Na pripojenie k Hive je potrebn\u00e9 internetov\u00e9 pripojenie.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "2fa": { + "data": { + "2fa": "Dvojfaktorov\u00fd k\u00f3d" + }, + "description": "Zadajte svoj overovac\u00ed k\u00f3d Hive. \n\n Ak chcete po\u017eiada\u0165 o \u010fal\u0161\u00ed k\u00f3d, zadajte k\u00f3d 0000.", + "title": "Dvojfaktorov\u00e9 overenie Hive." + }, + "configuration": { + "data": { + "device_name": "N\u00e1zov zariadenia" + }, + "description": "Zadajte konfigur\u00e1ciu Hive", + "title": "Konfigur\u00e1cia Hive." + }, + "reauth": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Znova zadajte svoje prihlasovacie \u00fadaje Hive.", + "title": "Prihl\u00e1senie do Hive" + }, + "user": { + "data": { + "password": "Heslo", + "scan_interval": "Interval skenovania (sekundy)", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte svoje prihlasovacie \u00fadaje Hive.", + "title": "Prihl\u00e1senie do Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Interval skenovania (sekundy)" + }, + "title": "Mo\u017enosti pre Hive" + } } } } \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/sk.json b/homeassistant/components/hlk_sw16/translations/sk.json index 5ada995aa6e..666f6e28840 100644 --- a/homeassistant/components/hlk_sw16/translations/sk.json +++ b/homeassistant/components/hlk_sw16/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/sk.json b/homeassistant/components/home_connect/translations/sk.json index c19b1a0b70c..370363ab123 100644 --- a/homeassistant/components/home_connect/translations/sk.json +++ b/homeassistant/components/home_connect/translations/sk.json @@ -1,7 +1,16 @@ { "config": { + "abort": { + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})" + }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/sk.json b/homeassistant/components/home_plus_control/translations/sk.json index 97ac5f20ed2..3cacfd05b98 100644 --- a/homeassistant/components/home_plus_control/translations/sk.json +++ b/homeassistant/components/home_plus_control/translations/sk.json @@ -1,10 +1,20 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 81d79b03c10..7e557e0808e 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -205,7 +205,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no ] if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) async_register_admin_service( hass, ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 317e9d1dfcd..06ecd4fe3ef 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "title": "The country has not been configured", + "description": "No country has been configured, please update the configuration by clicking on the \"learn more\" button below." + }, + "historic_currency": { + "title": "The configured currency is no longer in use", + "description": "The currency {currency} is no longer in use, please reconfigure the currency configuration." + }, + "python_version": { + "title": "Support for Python {current_python_version} is being removed", + "description": "Support for running Home Assistant in the current used Python version {current_python_version} is deprecated and will be removed in Home Assistant {breaks_in_ha_version}. Please upgrade Python to {required_python_version} to prevent your Home Assistant instance from breaking." + } + }, "system_health": { "info": { "arch": "CPU Architecture", diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json index 260c7bcb57c..d9dca0d27c0 100644 --- a/homeassistant/components/homeassistant/translations/bg.json +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -1,4 +1,17 @@ { + "issues": { + "country_not_configured": { + "description": "\u041d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430, \u043c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430, \u043a\u0430\u0442\u043e \u0449\u0440\u0430\u043a\u043d\u0435\u0442\u0435 \u0432\u044a\u0440\u0445\u0443 \u0431\u0443\u0442\u043e\u043d\u0430 \"\u043d\u0430\u0443\u0447\u0435\u0442\u0435 \u043f\u043e\u0432\u0435\u0447\u0435\" \u043f\u043e-\u0434\u043e\u043b\u0443.", + "title": "\u0414\u044a\u0440\u0436\u0430\u0432\u0430\u0442\u0430 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "historic_currency": { + "description": "\u0412\u0430\u043b\u0443\u0442\u0430\u0442\u0430 {currency} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 \u0432\u0430\u043b\u0443\u0442\u0430\u0442\u0430.", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u0432\u0430\u043b\u0443\u0442\u0430 \u0432\u0435\u0447\u0435 \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430" + }, + "python_version": { + "title": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430\u0442\u0430 \u0437\u0430 Python {current_python_version} \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + }, "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043d\u0430 CPU", diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index 8bcf5d96a86..3272c2fcbad 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -1,4 +1,17 @@ { + "issues": { + "country_not_configured": { + "description": "No s'ha configurat cap pa\u00eds, si us plau, actualitza la configuraci\u00f3 fent clic al bot\u00f3 \"m\u00e9s informaci\u00f3\" a continuaci\u00f3.", + "title": "El pa\u00eds no s'ha configurat" + }, + "historic_currency": { + "description": "El valor de moneda {currency} ja no s'utilitza, modifica la configuraci\u00f3 del valor de moneda.", + "title": "El valor de moneda configurat ja no s'utilitza" + }, + "python_version": { + "title": "S'est\u00e0 eliminant la compatibilitat amb Python {current_python_version}" + } + }, "system_health": { "info": { "arch": "Arquitectura de la CPU", diff --git a/homeassistant/components/homeassistant/translations/cs.json b/homeassistant/components/homeassistant/translations/cs.json index fbd96241e36..9aa26acbc2b 100644 --- a/homeassistant/components/homeassistant/translations/cs.json +++ b/homeassistant/components/homeassistant/translations/cs.json @@ -1,4 +1,14 @@ { + "issues": { + "historic_currency": { + "description": "M\u011bna {currency} se ji\u017e nepou\u017e\u00edv\u00e1, zm\u011b\u0148te pros\u00edm konfiguraci m\u011bny.", + "title": "Nakonfigurovan\u00e1 m\u011bna se ji\u017e nepou\u017e\u00edv\u00e1" + }, + "python_version": { + "description": "Podpora pro spou\u0161t\u011bn\u00ed Home Assistant v aktu\u00e1ln\u011b pou\u017e\u00edvan\u00e9 verzi Pythonu {current_python_version} je zastaral\u00e1 a bude odstran\u011bna v Home Assistant {breaks_in_ha_version}. Upgradujte pros\u00edm Python na {required_python_version}, abyste zabr\u00e1nili po\u0161kozen\u00ed instance Home Assistant.", + "title": "Podpora pro Python {current_python_version} se odstra\u0148uje" + } + }, "system_health": { "info": { "arch": "Architektura procesoru", diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index 756670e5a47..c38585adb19 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "Es wurde kein Land konfiguriert. Bitte aktualisiere die Konfiguration, indem du unten auf die Schaltfl\u00e4che \"Mehr erfahren\" klickst.", + "title": "Das Land wurde nicht konfiguriert" + }, + "historic_currency": { + "description": "Die W\u00e4hrung {currency} wird nicht mehr verwendet, bitte konfiguriere die W\u00e4hrungskonfiguration neu.", + "title": "Die konfigurierte W\u00e4hrung ist nicht mehr in Gebrauch" + }, + "python_version": { + "description": "Die Unterst\u00fctzung f\u00fcr die Ausf\u00fchrung von Home Assistant in der aktuell verwendeten Python-Version {current_python_version} ist veraltet und wird in Home Assistant {breaks_in_ha_version} entfernt. Bitte aktualisiere Python auf {required_python_version}, um zu verhindern, dass deine Home Assistant-Instanz besch\u00e4digt wird.", + "title": "Die Unterst\u00fctzung f\u00fcr Python {current_python_version} wird entfernt" + } + }, "system_health": { "info": { "arch": "CPU-Architektur", diff --git a/homeassistant/components/homeassistant/translations/el.json b/homeassistant/components/homeassistant/translations/el.json index 9c345d511e1..7fa0d674233 100644 --- a/homeassistant/components/homeassistant/translations/el.json +++ b/homeassistant/components/homeassistant/translations/el.json @@ -1,4 +1,14 @@ { + "issues": { + "historic_currency": { + "description": "\u03a4\u03bf \u03bd\u03cc\u03bc\u03b9\u03c3\u03bc\u03b1 {currency} \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd, \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bd\u03bf\u03bc\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2.", + "title": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03bf \u03bd\u03cc\u03bc\u03b9\u03c3\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd" + }, + "python_version": { + "description": "\u0397 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Python {current_python_version} \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant {breaks_in_ha_version} . \u0391\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd Python \u03c3\u03b5 {required_python_version} \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03c4\u03c1\u03ad\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 Home Assistant.", + "title": "\u0397 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd Python {current_python_version} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "system_health": { "info": { "arch": "\u0391\u03c1\u03c7\u03b9\u03c4\u03b5\u03ba\u03c4\u03bf\u03bd\u03b9\u03ba\u03ae CPU", diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 37c4498b32b..4b94cc37d2e 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "No country has been configured, please update the configuration by clicking on the \"learn more\" button below.", + "title": "The country has not been configured" + }, + "historic_currency": { + "description": "The currency {currency} is no longer in use, please reconfigure the currency configuration.", + "title": "The configured currency is no longer in use" + }, + "python_version": { + "description": "Support for running Home Assistant in the current used Python version {current_python_version} is deprecated and will be removed in Home Assistant {breaks_in_ha_version}. Please upgrade Python to {required_python_version} to prevent your Home Assistant instance from breaking.", + "title": "Support for Python {current_python_version} is being removed" + } + }, "system_health": { "info": { "arch": "CPU Architecture", diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 5b447f7177f..e7fe09ac9b9 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "No se ha configurado ning\u00fan pa\u00eds, por favor, actualiza la configuraci\u00f3n haciendo clic en el bot\u00f3n \"m\u00e1s informaci\u00f3n\" a continuaci\u00f3n.", + "title": "El pa\u00eds no ha sido configurado" + }, + "historic_currency": { + "description": "La moneda {currency} ya no est\u00e1 en uso, por favor, vuelve a configurar la moneda.", + "title": "La moneda configurada ya no est\u00e1 en uso" + }, + "python_version": { + "description": "La compatibilidad con la ejecuci\u00f3n de Home Assistant en la versi\u00f3n de Python utilizada actualmente {current_python_version} est\u00e1 obsoleta y se eliminar\u00e1 en Home Assistant {breaks_in_ha_version}. Actualiza Python a {required_python_version} para evitar que tu instancia de Home Assistant se rompa.", + "title": "Se va a eliminar la compatibilidad con Python {current_python_version}" + } + }, "system_health": { "info": { "arch": "Arquitectura de CPU", diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 7b9b675ed6f..a84e7ec3736 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "\u00dchtegi riiki pole m\u00e4\u00e4ratud, v\u00e4rskenda s\u00e4tteid kl\u00f5psates alloleval nupul \"Lisateave\".", + "title": "Riik pole m\u00e4\u00e4ratud" + }, + "historic_currency": { + "description": "Valuuta {currency} ei ole enam kasutusel, seadista valuuta uuesti.", + "title": "Seadistatud valuutat enam ei kasutata" + }, + "python_version": { + "description": "Home Assistanti k\u00e4itamise tugi praeguses kasutatavas Pythoni versioonis {current_python_version} on aegunud ja see eemaldatakse rakendusest Home Assistant {breaks_in_ha_version} . T\u00e4ienda Python versioonile {required_python_version} , et v\u00e4ltida Home Assistanti eksemplari t\u00f6\u00f6 l\u00f5ppemist.", + "title": "Pythoni {current_python_version} tugi eemaldatakse" + } + }, "system_health": { "info": { "arch": "Protsessori arhitektuur", diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json index 48e38978ba6..e4aa7daa31c 100644 --- a/homeassistant/components/homeassistant/translations/fr.json +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -1,4 +1,13 @@ { + "issues": { + "country_not_configured": { + "title": "Le pays n\u2019a pas \u00e9t\u00e9 configur\u00e9" + }, + "historic_currency": { + "description": "La devise {currency} n\u2019est plus utilis\u00e9e, veuillez actualiser la configuration de la devise.", + "title": "La devise configur\u00e9e n\u2019est plus utilis\u00e9e" + } + }, "system_health": { "info": { "arch": "Architecture du processeur", diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json index 20de5a2d1b7..75f487a964e 100644 --- a/homeassistant/components/homeassistant/translations/he.json +++ b/homeassistant/components/homeassistant/translations/he.json @@ -1,6 +1,9 @@ { "system_health": { "info": { + "arch": "\u05d0\u05e8\u05db\u05d9\u05d8\u05e7\u05d8\u05d5\u05e8\u05ea \u05de\u05e2\u05d1\u05d3", + "config_dir": "\u05e1\u05e4\u05e8\u05d9\u05d9\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4", + "dev": "\u05e4\u05d9\u05ea\u05d5\u05d7", "docker": "Docker", "hassio": "\u05de\u05e4\u05e7\u05d7", "installation_type": "\u05e1\u05d5\u05d2 \u05d4\u05ea\u05e7\u05e0\u05d4", @@ -9,7 +12,8 @@ "python_version": "\u05d2\u05e8\u05e1\u05ea \u05e4\u05d9\u05d9\u05ea\u05d5\u05df", "timezone": "\u05d0\u05d6\u05d5\u05e8 \u05d6\u05de\u05df", "user": "\u05de\u05e9\u05ea\u05de\u05e9", - "version": "\u05d2\u05d9\u05e8\u05e1\u05d4" + "version": "\u05d2\u05d9\u05e8\u05e1\u05d4", + "virtualenv": "\u05e1\u05d1\u05d9\u05d1\u05d4 \u05d5\u05d9\u05e8\u05d8\u05d5\u05d0\u05dc\u05d9\u05ea" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index 7261dfa1f7a..1e290293009 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -1,4 +1,14 @@ { + "issues": { + "historic_currency": { + "description": "{currency} p\u00e9nznem m\u00e1r nincs haszn\u00e1latban, k\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra a p\u00e9nznemet.", + "title": "A be\u00e1ll\u00edtott p\u00e9nznem m\u00e1r nincs haszn\u00e1latban" + }, + "python_version": { + "description": "A Home Assistant futtat\u00e1s\u00e1nak t\u00e1mogat\u00e1sa az aktu\u00e1lisan haszn\u00e1lt Python-verzi\u00f3ban {current_python_version} elavult, \u00e9s a Home Assistant {breaks_in_ha_version} verzi\u00f3j\u00e1ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rj\u00fck, friss\u00edtse a Pythont a {required_python_version} verzi\u00f3ra, hogy megakad\u00e1lyozza a Home Assistant j\u00f6v\u0151beni m\u0171k\u00f6d\u00e9sk\u00e9ptelens\u00e9g\u00e9t.", + "title": "A Python {current_python_version} t\u00e1mogat\u00e1sa megsz\u0171nik" + } + }, "system_health": { "info": { "arch": "Processzor architekt\u00fara", diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json index 7c2994d8bbb..ae90a09471f 100644 --- a/homeassistant/components/homeassistant/translations/id.json +++ b/homeassistant/components/homeassistant/translations/id.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "Belum ada negara yang dikonfigurasi, perbarui konfigurasi dengan mengeklik tombol \"pelajari selengkapnya\" di bawah ini.", + "title": "Negara belum dikonfigurasi" + }, + "historic_currency": { + "description": "Mata uang {currency} tidak lagi digunakan, konfigurasikan ulang konfigurasi mata uang.", + "title": "Mata uang yang dikonfigurasi tidak lagi digunakan" + }, + "python_version": { + "description": "Dukungan untuk menjalankan Home Assistant dalam versi Python yang digunakan saat ini {current_python_version} sudah usang dan akan dihapus di Home Assistant {breaks_in_ha_version}. Tingkatkan Python ke {required_python_version} untuk mencegah instans Home Assistant Anda rusak.", + "title": "Dukungan untuk Python {current_python_version} dalam proses penghapusan" + } + }, "system_health": { "info": { "arch": "Arsitektur CPU", diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 432fb9dea46..164f7e90c95 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -1,4 +1,14 @@ { + "issues": { + "historic_currency": { + "description": "La valuta {currency} non \u00e8 pi\u00f9 in uso, riconfigura la configurazione della valuta.", + "title": "La valuta configurata non \u00e8 pi\u00f9 in uso" + }, + "python_version": { + "description": "Il supporto per l'esecuzione di Home Assistant nell'attuale versione di Python utilizzata {current_python_version} \u00e8 deprecato e verr\u00e0 rimosso in Home Assistant {breaks_in_ha_version}. Aggiorna Python a {required_python_version} per evitare che la tua istanza di Home Assistant non funzioni pi\u00f9.", + "title": "Il supporto per Python {current_python_version} \u00e8 stato rimosso" + } + }, "system_health": { "info": { "arch": "Architettura della CPU", diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 0bafc7a3959..156c6fe4f1e 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -1,4 +1,10 @@ { + "issues": { + "python_version": { + "description": "Ondersteuning om Home Assistant met de huidige Python (versie {current_python_version}) gaat vervallen en zal vervalleen in Home Assistant {breaks_in_ha_version}. Werk de Python software bij naar minimaal versie {required_python_version} om te voorkomen dat je Home Assistant instantie niet meer goed werkt.", + "title": "Ondersteuning voor Python {current_python_version} gaat vervallen" + } + }, "system_health": { "info": { "arch": "CPU-architectuur", diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 0932c2e75a6..2ba575f693b 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "Ingen land er konfigurert, vennligst oppdater konfigurasjonen ved \u00e5 klikke p\u00e5 \"les mer\"-knappen nedenfor.", + "title": "Landet er ikke konfigurert" + }, + "historic_currency": { + "description": "Valutaen {currency} er ikke lenger i bruk, vennligst konfigurer valutakonfigurasjonen p\u00e5 nytt.", + "title": "Tollet er ugyldig eller ikke lenger autorisert." + }, + "python_version": { + "description": "St\u00f8tte for \u00e5 kj\u00f8re Home Assistant i den gjeldende brukte Python-versjonen {current_python_version} er avviklet og vil bli fjernet i Home Assistant {breaks_in_ha_version} . Oppgrader Python til {required_python_version} for \u00e5 forhindre at Home Assistant-forekomsten din g\u00e5r i stykker.", + "title": "St\u00f8tte for Python {current_python_version} blir fjernet" + } + }, "system_health": { "info": { "arch": "CPU-arkitektur", diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index bdf26a8b49d..02383146b53 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -1,4 +1,14 @@ { + "issues": { + "historic_currency": { + "description": "Waluta {currency} nie jest ju\u017c u\u017cywana. Zmie\u0144 konfiguracj\u0119 waluty.", + "title": "Skonfigurowana waluta nie jest ju\u017c u\u017cywana" + }, + "python_version": { + "description": "Obs\u0142uga uruchamiania Home Assistanta w obecnie u\u017cywanej wersji Pythona {current_python_version} jest przestarza\u0142a i zostanie usuni\u0119ta w Home Assistant {breaks_in_ha_version}. Zaktualizuj Pythona do wersji {required_python_version}, aby zapobiec awarii Home Assistanta.", + "title": "Obs\u0142uga Pythona w wersji {current_python_version} zostanie usuni\u0119ta" + } + }, "system_health": { "info": { "arch": "Architektura procesora", diff --git a/homeassistant/components/homeassistant/translations/pt-BR.json b/homeassistant/components/homeassistant/translations/pt-BR.json index 5ea540d67f3..fa31c37dd58 100644 --- a/homeassistant/components/homeassistant/translations/pt-BR.json +++ b/homeassistant/components/homeassistant/translations/pt-BR.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "Nenhum pa\u00eds foi configurado, atualize a configura\u00e7\u00e3o clicando no bot\u00e3o \"saiba mais\" abaixo.", + "title": "O pa\u00eds n\u00e3o foi configurado" + }, + "historic_currency": { + "description": "A moeda {currency} n\u00e3o est\u00e1 mais em uso, reconfigure a configura\u00e7\u00e3o da moeda.", + "title": "A moeda configurada n\u00e3o est\u00e1 mais em uso" + }, + "python_version": { + "description": "O suporte para executar o Home Assistant na vers\u00e3o atual do Python usada {current_python_version} est\u00e1 obsoleto e ser\u00e1 removido no Home Assistant {breaks_in_ha_version} . Atualize o Python para {required_python_version} para evitar que sua inst\u00e2ncia do Home Assistant seja interrompida.", + "title": "O suporte para Python {current_python_version} est\u00e1 sendo removido" + } + }, "system_health": { "info": { "arch": "Arquitetura da CPU", diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json index b0e27b70861..d368a980a56 100644 --- a/homeassistant/components/homeassistant/translations/ru.json +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0441\u0442\u0440\u0430\u043d\u0435 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e, \u043d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443 \"\u0423\u0437\u043d\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435\".", + "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0441\u0442\u0440\u0430\u043d\u0435 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430" + }, + "historic_currency": { + "description": "\u0412\u0430\u043b\u044e\u0442\u0430 {currency} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0432\u0430\u043b\u044e\u0442\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0430\u044f \u0432\u0430\u043b\u044e\u0442\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f" + }, + "python_version": { + "description": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0437\u0430\u043f\u0443\u0441\u043a\u0430 Home Assistant \u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0439 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 Python {current_python_version} \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 {breaks_in_ha_version}. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 Python \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 {required_python_version}.", + "title": "\u041f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 Python \u0432\u0435\u0440\u0441\u0438\u0438 {current_python_version}" + } + }, "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", diff --git a/homeassistant/components/homeassistant/translations/sk.json b/homeassistant/components/homeassistant/translations/sk.json new file mode 100644 index 00000000000..59d0db98bb1 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/sk.json @@ -0,0 +1,28 @@ +{ + "issues": { + "country_not_configured": { + "description": "Nebola nakonfigurovan\u00e1 \u017eiadna krajina, aktualizujte konfigur\u00e1ciu kliknut\u00edm na tla\u010didlo \u201eviac inform\u00e1ci\u00ed\u201c ni\u017e\u0161ie.", + "title": "Krajina nebola nakonfigurovan\u00e1" + }, + "historic_currency": { + "title": "Konfigurovan\u00e1 mena sa u\u017e nepou\u017e\u00edva" + }, + "python_version": { + "title": "Podpora pre Python {current_python_version} sa odstra\u0148uje" + } + }, + "system_health": { + "info": { + "arch": "Architekt\u00fara CPU", + "config_dir": "Adres\u00e1r konfigur\u00e1cie", + "docker": "Docker", + "installation_type": "Typ in\u0161tal\u00e1cie", + "os_name": "Rodina opera\u010dn\u00fdch syst\u00e9mov", + "os_version": "Verzia opera\u010dn\u00e9ho syst\u00e9mu", + "python_version": "Verzia Pythonu", + "timezone": "\u010casov\u00e9 p\u00e1smo", + "user": "Pou\u017e\u00edvate\u013e", + "version": "Verzia" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index c42acf960c6..d1f518daeaa 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "\u5c1a\u672a\u8a2d\u5b9a\u570b\u5bb6\u3001\u8acb\u9ede\u9078\u4e0b\u65b9 \"\u4e86\u89e3\u66f4\u591a\" \u6309\u9215\u4ee5\u66f4\u65b0\u8a2d\u5b9a\u3002", + "title": "\u5c1a\u672a\u8a2d\u5b9a\u570b\u5bb6" + }, + "historic_currency": { + "description": "\u8ca8\u5e63 {currency} \u4e0d\u518d\u4f7f\u7528\u3001\u8acb\u91cd\u65b0\u8a2d\u5b9a\u5176\u4ed6\u8ca8\u5e63\u3002", + "title": "\u8a2d\u5b9a\u8ca8\u5e63\u4e0d\u518d\u4f7f\u7528" + }, + "python_version": { + "description": "Support for \u57f7\u884c Home Assistant \u76ee\u524d\u652f\u63f4\u4f7f\u7528\u4e4b Python v{current_python_version} \u7248\u5df2\u7d93\u505c\u6b62\u652f\u63f4\u3001\u5373\u5c07\u65bc Home Assistant {breaks_in_ha_version} \u7248\u9032\u884c\u79fb\u9664\u3002\u8acb\u66f4\u65b0 Python \u81f3 v{required_python_version} \u7248\u4ee5\u907f\u514d Home Assistant \u7121\u6cd5\u57f7\u884c\u3002", + "title": "Python {current_python_version} \u7248\u652f\u63f4\u5373\u5c07\u79fb\u9664" + } + }, "system_health": { "info": { "arch": "CPU \u67b6\u69cb", diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index ed7957407a8..82631d58ec5 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -9,6 +9,7 @@ import logging import aiohttp from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +from homeassistant.components.hassio import get_supervisor_info, is_hassio from homeassistant.const import __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -142,6 +143,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) __version__, ensure_strategy=AwesomeVersionStrategy.CALVER, ) + self.supervisor = is_hassio(self.hass) async def _async_update_data(self) -> dict[str, IntegrationAlert]: response = await async_get_clientsession(self.hass).get( @@ -170,6 +172,23 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) if self.ha_version >= resolved_in_version: continue + if self.supervisor and "supervisor" in alert: + if (supervisor_info := get_supervisor_info(self.hass)) is None: + continue + + if "affected_from_version" in alert["supervisor"]: + affected_from_version = AwesomeVersion( + alert["supervisor"]["affected_from_version"], + ) + if supervisor_info["version"] < affected_from_version: + continue + if "resolved_in_version" in alert["supervisor"]: + resolved_in_version = AwesomeVersion( + alert["supervisor"]["resolved_in_version"], + ) + if supervisor_info["version"] >= resolved_in_version: + continue + for integration in alert["integrations"]: if "package" not in integration: continue diff --git a/homeassistant/components/homeassistant_alerts/translations/sk.json b/homeassistant/components/homeassistant_alerts/translations/sk.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/sk.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_hardware/__init__.py b/homeassistant/components/homeassistant_hardware/__init__.py new file mode 100644 index 00000000000..f3a63a7f767 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/__init__.py @@ -0,0 +1,10 @@ +"""The Home Assistant Hardware integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the component.""" + return True diff --git a/homeassistant/components/homeassistant_hardware/const.py b/homeassistant/components/homeassistant_hardware/const.py new file mode 100644 index 00000000000..dd3a254d097 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/const.py @@ -0,0 +1,7 @@ +"""Constants for the Homeassistant Hardware integration.""" + +import logging + +LOGGER = logging.getLogger(__package__) + +SILABS_MULTIPROTOCOL_ADDON_SLUG = "core_silabs_multiprotocol" diff --git a/homeassistant/components/homeassistant_hardware/manifest.json b/homeassistant/components/homeassistant_hardware/manifest.json new file mode 100644 index 00000000000..722306720a5 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "homeassistant_hardware", + "name": "Home Assistant Hardware", + "documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware", + "codeowners": ["@home-assistant/core"], + "integration_type": "system", + "after_dependencies": ["zha"] +} diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py new file mode 100644 index 00000000000..500555cc6ae --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -0,0 +1,358 @@ +"""Manage the Silicon Labs Multiprotocol add-on.""" +from __future__ import annotations + +from abc import abstractmethod +import asyncio +import dataclasses +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + is_hassio, +) +from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN +from homeassistant.components.zha.radio_manager import ZhaMultiPANMigrationHelper +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import ( + AbortFlow, + FlowHandler, + FlowManager, + FlowResult, +) +from homeassistant.helpers.singleton import singleton + +from .const import LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG + +_LOGGER = logging.getLogger(__name__) + +DATA_ADDON_MANAGER = "silabs_multiprotocol_addon_manager" + +ADDON_SETUP_TIMEOUT = 5 +ADDON_SETUP_TIMEOUT_ROUNDS = 40 + +CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware" +CONF_ADDON_DEVICE = "device" +CONF_ENABLE_MULTI_PAN = "enable_multi_pan" + + +@singleton(DATA_ADDON_MANAGER) +@callback +def get_addon_manager(hass: HomeAssistant) -> AddonManager: + """Get the add-on manager.""" + return AddonManager( + hass, + LOGGER, + "Silicon Labs Multiprotocol", + SILABS_MULTIPROTOCOL_ADDON_SLUG, + ) + + +@dataclasses.dataclass +class SerialPortSettings: + """Serial port settings.""" + + device: str + baudrate: str + flow_control: bool + + +def get_zigbee_socket(hass, addon_info: AddonInfo) -> str: + """Return the zigbee socket. + + Raises AddonError on error + """ + return f"socket://{addon_info.hostname}:9999" + + +class BaseMultiPanFlow(FlowHandler): + """Support configuring the Silicon Labs Multiprotocol add-on.""" + + def __init__(self) -> None: + """Set up flow instance.""" + # If we install the add-on we should uninstall it on entry remove. + self.install_task: asyncio.Task | None = None + self.start_task: asyncio.Task | None = None + self._zha_migration_mgr: ZhaMultiPANMigrationHelper | None = None + + @property + @abstractmethod + def flow_manager(self) -> FlowManager: + """Return the flow manager of the flow.""" + + @abstractmethod + async def _async_serial_port_settings(self) -> SerialPortSettings: + """Return the radio serial port settings.""" + + @abstractmethod + async def _async_zha_physical_discovery(self) -> dict[str, Any]: + """Return ZHA discovery data when multiprotocol FW is not used. + + Passed to ZHA do determine if the ZHA config entry is connected to the radio + being migrated. + """ + + @abstractmethod + def _hardware_name(self) -> str: + """Return the name of the hardware.""" + + @abstractmethod + def _zha_name(self) -> str: + """Return the ZHA name.""" + + async def async_step_install_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Install Silicon Labs Multiprotocol add-on.""" + 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" + ) + + try: + await self.install_task + except AddonError as err: + self.install_task = None + _LOGGER.error(err) + return self.async_show_progress_done(next_step_id="install_failed") + + self.install_task = None + + return self.async_show_progress_done(next_step_id="configure_addon") + + async def async_step_install_failed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Add-on installation failed.""" + return self.async_abort(reason="addon_install_failed") + + async def async_step_start_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Start Silicon Labs Multiprotocol add-on.""" + if not self.start_task: + self.start_task = self.hass.async_create_task(self._async_start_addon()) + return self.async_show_progress( + step_id="start_addon", progress_action="start_addon" + ) + + try: + await self.start_task + except (AddonError, AbortFlow) as err: + self.start_task = None + _LOGGER.error(err) + return self.async_show_progress_done(next_step_id="start_failed") + + self.start_task = None + return self.async_show_progress_done(next_step_id="finish_addon_setup") + + async def async_step_start_failed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Add-on start failed.""" + return self.async_abort(reason="addon_start_failed") + + async def _async_start_addon(self) -> None: + """Start Silicon Labs Multiprotocol add-on.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + try: + await addon_manager.async_schedule_start_addon() + finally: + # Continue the flow after show progress when the task is done. + self.hass.async_create_task( + self.flow_manager.async_configure(flow_id=self.flow_id) + ) + + @abstractmethod + async def async_step_configure_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure the Silicon Labs Multiprotocol add-on.""" + + @abstractmethod + async def async_step_finish_addon_setup( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Finish setup of the Silicon Labs Multiprotocol add-on.""" + + async def _async_get_addon_info(self) -> AddonInfo: + """Return and cache Silicon Labs Multiprotocol add-on info.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + try: + addon_info: AddonInfo = await addon_manager.async_get_addon_info() + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow("addon_info_failed") from err + + return addon_info + + async def _async_set_addon_config(self, config: dict) -> None: + """Set Silicon Labs Multiprotocol add-on config.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + try: + await addon_manager.async_set_addon_options(config) + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow("addon_set_config_failed") from err + + async def _async_install_addon(self) -> None: + """Install the Silicon Labs Multiprotocol add-on.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + try: + await addon_manager.async_schedule_install_addon() + finally: + # Continue the flow after show progress when the task is done. + self.hass.async_create_task( + self.flow_manager.async_configure(flow_id=self.flow_id) + ) + + +class OptionsFlowHandler(BaseMultiPanFlow, config_entries.OptionsFlow): + """Handle an options flow for the Silicon Labs Multiprotocol add-on.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Set up the options flow.""" + super().__init__() + self.config_entry = config_entry + self.original_addon_config: dict[str, Any] | None = None + self.revert_reason: str | None = None + + @property + def flow_manager(self) -> config_entries.OptionsFlowManager: + """Return the correct flow manager.""" + return self.hass.config_entries.options + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if not is_hassio(self.hass): + return self.async_abort(reason="not_hassio") + + return await self.async_step_on_supervisor() + + async def async_step_on_supervisor( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle logic when on Supervisor host.""" + addon_info = await self._async_get_addon_info() + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_addon_not_installed() + return await self.async_step_addon_installed() + + async def async_step_addon_not_installed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle logic when the addon is not yet installed.""" + if user_input is None: + return self.async_show_form( + step_id="addon_not_installed", + data_schema=vol.Schema( + {vol.Required(CONF_ENABLE_MULTI_PAN, default=False): bool} + ), + description_placeholders={"hardware_name": self._hardware_name()}, + ) + if not user_input[CONF_ENABLE_MULTI_PAN]: + return self.async_create_entry(title="", data={}) + + return await self.async_step_install_addon() + + async def async_step_configure_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure the Silicon Labs Multiprotocol add-on.""" + addon_info = await self._async_get_addon_info() + + addon_config = addon_info.options + + serial_port_settings = await self._async_serial_port_settings() + new_addon_config = { + **addon_config, + CONF_ADDON_AUTOFLASH_FW: True, + **dataclasses.asdict(serial_port_settings), + } + + # Initiate ZHA migration + zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN) + + if zha_entries: + zha_migration_mgr = ZhaMultiPANMigrationHelper(self.hass, zha_entries[0]) + migration_data = { + "new_discovery_info": { + "name": self._zha_name(), + "port": { + "path": get_zigbee_socket(self.hass, addon_info), + }, + "radio_type": "ezsp", + }, + "old_discovery_info": await self._async_zha_physical_discovery(), + } + _LOGGER.debug("Starting ZHA migration with: %s", migration_data) + try: + if await zha_migration_mgr.async_initiate_migration(migration_data): + self._zha_migration_mgr = zha_migration_mgr + except Exception as err: + _LOGGER.exception("Unexpected exception during ZHA migration") + raise AbortFlow("zha_migration_failed") from err + + if new_addon_config != addon_config: + # Copy the add-on config to keep the objects separate. + self.original_addon_config = dict(addon_config) + _LOGGER.debug("Reconfiguring addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config) + + return await self.async_step_start_addon() + + async def async_step_finish_addon_setup( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Prepare info needed to complete the config entry update.""" + # Always reload entry after installing the addon. + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) + + # Finish ZHA migration if needed + if self._zha_migration_mgr: + try: + await self._zha_migration_mgr.async_finish_migration() + except Exception as err: + _LOGGER.exception("Unexpected exception during ZHA migration") + raise AbortFlow("zha_migration_failed") from err + + return self.async_create_entry(title="", data={}) + + async def async_step_addon_installed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle logic when the addon is already installed.""" + addon_info = await self._async_get_addon_info() + + serial_device = (await self._async_serial_port_settings()).device + if addon_info.options.get(CONF_ADDON_DEVICE) == serial_device: + return await self.async_step_show_revert_guide() + return await self.async_step_addon_installed_other_device() + + async def async_step_show_revert_guide( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Link to a guide for reverting to Zigbee firmware.""" + if user_input is None: + return self.async_show_form(step_id="show_revert_guide") + return self.async_create_entry(title="", data={}) + + async def async_step_addon_installed_other_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Show dialog explaining the addon is in use by another device.""" + if user_input is None: + return self.async_show_form(step_id="addon_installed_other_device") + return self.async_create_entry(title="", data={}) diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json new file mode 100644 index 00000000000..47549794fc8 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -0,0 +1,43 @@ +{ + "silabs_multiprotocol_hardware": { + "options": { + "step": { + "addon_not_installed": { + "title": "Enable multiprotocol support on the IEEE 802.15.4 radio", + "description": "When multiprotocol support is enabled, the {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: This is an experimental feature.", + "data": { + "enable_multi_pan": "Enable multiprotocol support" + } + }, + "addon_installed_other_device": { + "title": "Multiprotocol support is already enabled for another device" + }, + "install_addon": { + "title": "The Silicon Labs Multiprotocol add-on installation has started" + }, + "show_revert_guide": { + "title": "Multiprotocol support is enabled for this device", + "description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio" + }, + "start_addon": { + "title": "The Silicon Labs Multiprotocol add-on is starting." + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "addon_info_failed": "Failed to get Silicon Labs Multiprotocol add-on info.", + "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", + "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", + "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "not_hassio": "The hardware options can only be configured on HassOS installations.", + "zha_migration_failed": "The ZHA migration did not succeed." + }, + "progress": { + "install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Silicon Labs Multiprotocol add-on start completes. This may take some seconds." + } + } + } +} diff --git a/homeassistant/components/homeassistant_hardware/translations/en.json b/homeassistant/components/homeassistant_hardware/translations/en.json new file mode 100644 index 00000000000..ec75e234c4d --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/translations/en.json @@ -0,0 +1,43 @@ +{ + "silabs_multiprotocol_hardware": { + "options": { + "abort": { + "addon_info_failed": "Failed to get Silicon Labs Multiprotocol add-on info.", + "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", + "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", + "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "not_hassio": "The hardware options can only be configured on HassOS installations.", + "zha_migration_failed": "The ZHA migration did not succeed." + }, + "error": { + "unknown": "Unexpected error" + }, + "progress": { + "install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Silicon Labs Multiprotocol add-on start completes. This may take some seconds." + }, + "step": { + "addon_installed_other_device": { + "title": "Multiprotocol support is already enabled for another device" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Enable multiprotocol support" + }, + "description": "When multiprotocol support is enabled, the {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: This is an experimental feature.", + "title": "Enable multiprotocol support on the IEEE 802.15.4 radio" + }, + "install_addon": { + "title": "The Silicon Labs Multiprotocol add-on installation has started" + }, + "show_revert_guide": { + "description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio", + "title": "Multiprotocol support is enabled for this device" + }, + "start_addon": { + "title": "The Silicon Labs Multiprotocol add-on is starting." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index dd4cf013fab..e65394ca15c 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -1,32 +1,102 @@ """The Home Assistant Sky Connect integration.""" from __future__ import annotations -from typing import cast +import logging from homeassistant.components import usb +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + is_hassio, +) +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + get_addon_manager, + get_zigbee_socket, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from .const import DOMAIN +from .util import get_usb_service_info + +_LOGGER = logging.getLogger(__name__) + + +async def _multi_pan_addon_info(hass, entry: ConfigEntry) -> AddonInfo | None: + """Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect.""" + if not is_hassio(hass): + return None + + addon_manager: AddonManager = get_addon_manager(hass) + try: + addon_info: AddonInfo = await addon_manager.async_get_addon_info() + except AddonError as err: + _LOGGER.error(err) + raise ConfigEntryNotReady from err + + # Start the addon if it's not started + if addon_info.state == AddonState.NOT_RUNNING: + await addon_manager.async_start_addon() + + if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.RUNNING): + _LOGGER.debug( + "Multi pan addon in state %s, delaying yellow config entry setup", + addon_info.state, + ) + raise ConfigEntryNotReady + + if addon_info.state == AddonState.NOT_INSTALLED: + return None + + usb_dev = entry.data["device"] + dev_path = await hass.async_add_executor_job(usb.get_serial_by_id, usb_dev) + + if addon_info.options["device"] != dev_path: + return None + + return addon_info + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant Sky Connect config entry.""" - usb_info = usb.UsbServiceInfo( - device=entry.data["device"], - vid=entry.data["vid"], - pid=entry.data["pid"], - serial_number=entry.data["serial_number"], - manufacturer=entry.data["manufacturer"], - description=entry.data["description"], + matcher = usb.USBCallbackMatcher( + domain=DOMAIN, + vid=entry.data["vid"].upper(), + pid=entry.data["pid"].upper(), + serial_number=entry.data["serial_number"].lower(), + manufacturer=entry.data["manufacturer"].lower(), + description=entry.data["description"].lower(), ) - if not usb.async_is_plugged_in(hass, cast(usb.USBCallbackMatcher, entry.data)): + + if not usb.async_is_plugged_in(hass, matcher): # The USB dongle is not plugged in raise ConfigEntryNotReady + addon_info = await _multi_pan_addon_info(hass, entry) + + if not addon_info: + usb_info = get_usb_service_info(entry) + await hass.config_entries.flow.async_init( + "zha", + context={"source": "usb"}, + data=usb_info, + ) + return True + + hw_discovery_data = { + "name": "Sky Connect Multi-PAN", + "port": { + "path": get_zigbee_socket(hass, addon_info), + }, + "radio_type": "ezsp", + } await hass.config_entries.flow.async_init( "zha", - context={"source": "usb"}, - data=usb_info, + context={"source": "hardware"}, + data=hw_discovery_data, ) return True diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 21cc5e3ace4..1e4fd8701cd 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -1,11 +1,16 @@ """Config flow for the Home Assistant Sky Connect integration.""" from __future__ import annotations +from typing import Any + from homeassistant.components import usb -from homeassistant.config_entries import ConfigFlow +from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN +from .util import get_usb_service_info class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): @@ -13,6 +18,14 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> HomeAssistantSkyConnectOptionsFlow: + """Return the options flow.""" + return HomeAssistantSkyConnectOptionsFlow(config_entry) + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle usb discovery.""" device = discovery_info.device @@ -35,3 +48,35 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): "description": description, }, ) + + +class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): + """Handle an option flow for Home Assistant Sky Connect.""" + + async def _async_serial_port_settings( + self, + ) -> silabs_multiprotocol_addon.SerialPortSettings: + """Return the radio serial port settings.""" + usb_dev = self.config_entry.data["device"] + dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, usb_dev) + return silabs_multiprotocol_addon.SerialPortSettings( + device=dev_path, + baudrate="115200", + flow_control=True, + ) + + async def _async_zha_physical_discovery(self) -> dict[str, Any]: + """Return ZHA discovery data when multiprotocol FW is not used. + + Passed to ZHA do determine if the ZHA config entry is connected to the radio + being migrated. + """ + return {"usb": get_usb_service_info(self.config_entry)} + + def _zha_name(self) -> str: + """Return the ZHA name.""" + return "Sky Connect Multi-PAN" + + def _hardware_name(self) -> str: + """Return the name of the hardware.""" + return "Home Assistant Sky Connect" diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index 6eceb746756..f48e1763dd5 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -17,6 +17,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: return [ HardwareInfo( board=None, + config_entries=[entry.entry_id], dongle=USBInfo( vid=entry.data["vid"], pid=entry.data["pid"], diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index 5ccb8bd5331..34bb2ad701c 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -1,9 +1,9 @@ { "domain": "homeassistant_sky_connect", "name": "Home Assistant Sky Connect", - "config_flow": false, + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", - "dependencies": ["hardware", "usb"], + "dependencies": ["hardware", "usb", "homeassistant_hardware"], "codeowners": ["@home-assistant/core"], "integration_type": "hardware", "usb": [ diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json new file mode 100644 index 00000000000..970f9d97a4c --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -0,0 +1,41 @@ +{ + "options": { + "step": { + "addon_not_installed": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::description%]", + "data": { + "enable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::data::enable_multi_pan%]" + } + }, + "addon_installed_other_device": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]" + }, + "install_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" + }, + "show_revert_guide": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" + }, + "start_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" + } + }, + "error": { + "unknown": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::error::unknown%]" + }, + "abort": { + "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" + }, + "progress": { + "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", + "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]" + } + } +} diff --git a/homeassistant/components/homeassistant_sky_connect/translations/en.json b/homeassistant/components/homeassistant_sky_connect/translations/en.json new file mode 100644 index 00000000000..8e12173f86a --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/translations/en.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Failed to get Silicon Labs Multiprotocol add-on info.", + "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", + "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", + "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "not_hassio": "The hardware options can only be configured on HassOS installations.", + "zha_migration_failed": "The ZHA migration did not succeed." + }, + "error": { + "unknown": "Unexpected error" + }, + "progress": { + "install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Silicon Labs Multiprotocol add-on start completes. This may take some seconds." + }, + "step": { + "addon_installed_other_device": { + "title": "Multiprotocol support is already enabled for another device" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Enable multiprotocol support" + }, + "description": "When multiprotocol support is enabled, the {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: This is an experimental feature.", + "title": "Enable multiprotocol support on the IEEE 802.15.4 radio" + }, + "install_addon": { + "title": "The Silicon Labs Multiprotocol add-on installation has started" + }, + "show_revert_guide": { + "description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio", + "title": "Multiprotocol support is enabled for this device" + }, + "start_addon": { + "title": "The Silicon Labs Multiprotocol add-on is starting." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py new file mode 100644 index 00000000000..804ce83d063 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -0,0 +1,17 @@ +"""Utility functions for Home Assistant Sky Connect integration.""" +from __future__ import annotations + +from homeassistant.components import usb +from homeassistant.config_entries import ConfigEntry + + +def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: + """Return UsbServiceInfo.""" + return usb.UsbServiceInfo( + device=config_entry.data["device"], + vid=config_entry.data["vid"], + pid=config_entry.data["pid"], + serial_number=config_entry.data["serial_number"], + manufacturer=config_entry.data["manufacturer"], + description=config_entry.data["description"], + ) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index e6eaa2f7fce..6099dc014df 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -1,11 +1,56 @@ """The Home Assistant Yellow integration.""" from __future__ import annotations -from homeassistant.components.hassio import get_os_info +import logging + +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + get_os_info, +) +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + get_addon_manager, + get_zigbee_socket, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from .const import RADIO_DEVICE, ZHA_HW_DISCOVERY_DATA + +_LOGGER = logging.getLogger(__name__) + + +async def _multi_pan_addon_info(hass, entry: ConfigEntry) -> AddonInfo | None: + """Return AddonInfo if the multi-PAN addon is enabled for the Yellow's radio.""" + addon_manager: AddonManager = get_addon_manager(hass) + try: + addon_info: AddonInfo = await addon_manager.async_get_addon_info() + except AddonError as err: + _LOGGER.error(err) + raise ConfigEntryNotReady from err + + # Start the addon if it's not started + if addon_info.state == AddonState.NOT_RUNNING: + await addon_manager.async_start_addon() + + if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.RUNNING): + _LOGGER.debug( + "Multi pan addon in state %s, delaying yellow config entry setup", + addon_info.state, + ) + raise ConfigEntryNotReady + + if addon_info.state == AddonState.NOT_INSTALLED: + return None + + if addon_info.options["device"] != RADIO_DEVICE: + return None + + return addon_info + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant Yellow config entry.""" @@ -19,18 +64,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) return False + addon_info = await _multi_pan_addon_info(hass, entry) + + if not addon_info: + hw_discovery_data = ZHA_HW_DISCOVERY_DATA + else: + hw_discovery_data = { + "name": "Yellow Multi-PAN", + "port": { + "path": get_zigbee_socket(hass, addon_info), + }, + "radio_type": "ezsp", + } + await hass.config_entries.flow.async_init( "zha", context={"source": "hardware"}, - data={ - "name": "Yellow", - "port": { - "path": "/dev/ttyAMA1", - "baudrate": 115200, - "flow_control": "hardware", - }, - "radio_type": "efr32", - }, + data=hw_discovery_data, ) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 191a28f47a4..09cdcc1469a 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -3,10 +3,12 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow +from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN +from .const import DOMAIN, ZHA_HW_DISCOVERY_DATA class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN): @@ -14,9 +16,47 @@ class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> HomeAssistantYellowOptionsFlow: + """Return the options flow.""" + return HomeAssistantYellowOptionsFlow(config_entry) + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return self.async_create_entry(title="Home Assistant Yellow", data={}) + + +class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): + """Handle an option flow for Home Assistant Yellow.""" + + async def _async_serial_port_settings( + self, + ) -> silabs_multiprotocol_addon.SerialPortSettings: + """Return the radio serial port settings.""" + return silabs_multiprotocol_addon.SerialPortSettings( + device="/dev/ttyAMA1", + baudrate="115200", + flow_control=True, + ) + + async def _async_zha_physical_discovery(self) -> dict[str, Any]: + """Return ZHA discovery data when multiprotocol FW is not used. + + Passed to ZHA do determine if the ZHA config entry is connected to the radio + being migrated. + """ + return {"hw": ZHA_HW_DISCOVERY_DATA} + + def _zha_name(self) -> str: + """Return the ZHA name.""" + return "Yellow Multi-PAN" + + def _hardware_name(self) -> str: + """Return the name of the hardware.""" + return "Home Assistant Yellow" diff --git a/homeassistant/components/homeassistant_yellow/const.py b/homeassistant/components/homeassistant_yellow/const.py index 41eae70b3f2..8f1f9a4c2b8 100644 --- a/homeassistant/components/homeassistant_yellow/const.py +++ b/homeassistant/components/homeassistant_yellow/const.py @@ -1,3 +1,14 @@ """Constants for the Home Assistant Yellow integration.""" DOMAIN = "homeassistant_yellow" + +RADIO_DEVICE = "/dev/ttyAMA1" +ZHA_HW_DISCOVERY_DATA = { + "name": "Yellow", + "port": { + "path": RADIO_DEVICE, + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", +} diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index ad17eccfe7f..b67eb50ff2c 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -6,6 +6,8 @@ from homeassistant.components.hassio import get_os_info from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from .const import DOMAIN + BOARD_NAME = "Home Assistant Yellow" MANUFACTURER = "homeassistant" MODEL = "yellow" @@ -22,6 +24,10 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: if not board == "yellow": raise HomeAssistantError + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + return [ HardwareInfo( board=BoardInfo( @@ -30,6 +36,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: model=MODEL, revision=None, ), + config_entries=config_entries, dongle=None, name=BOARD_NAME, url=None, diff --git a/homeassistant/components/homeassistant_yellow/manifest.json b/homeassistant/components/homeassistant_yellow/manifest.json index 47e6c8e2cd8..ef708e9429a 100644 --- a/homeassistant/components/homeassistant_yellow/manifest.json +++ b/homeassistant/components/homeassistant_yellow/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Yellow", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow", - "dependencies": ["hardware", "hassio"], + "dependencies": ["hardware", "hassio", "homeassistant_hardware"], "codeowners": ["@home-assistant/core"], "integration_type": "hardware" } diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json new file mode 100644 index 00000000000..970f9d97a4c --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -0,0 +1,41 @@ +{ + "options": { + "step": { + "addon_not_installed": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::description%]", + "data": { + "enable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_not_installed::data::enable_multi_pan%]" + } + }, + "addon_installed_other_device": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_installed_other_device::title%]" + }, + "install_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" + }, + "show_revert_guide": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" + }, + "start_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" + } + }, + "error": { + "unknown": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::error::unknown%]" + }, + "abort": { + "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" + }, + "progress": { + "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", + "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]" + } + } +} diff --git a/homeassistant/components/homeassistant_yellow/translations/bg.json b/homeassistant/components/homeassistant_yellow/translations/bg.json new file mode 100644 index 00000000000..4c009671060 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/bg.json @@ -0,0 +1,39 @@ +{ + "options": { + "abort": { + "addon_info_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 Silicon Labs Multiprotocol.", + "addon_install_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 Silicon Labs Multiprotocol.", + "addon_set_config_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Silicon Labs Multiprotocol.", + "addon_start_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 Silicon Labs Multiprotocol.", + "not_hassio": "\u0425\u0430\u0440\u0434\u0443\u0435\u0440\u043d\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u0438 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442 \u0441\u0430\u043c\u043e \u0437\u0430 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u0438 \u043d\u0430 HassOS.", + "zha_migration_failed": "\u041c\u0438\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 ZHA \u043d\u0435 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430." + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "progress": { + "install_addon": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0447\u0430\u043a\u0430\u0439\u0442\u0435, \u0434\u043e\u043a\u0430\u0442\u043e \u0437\u0430\u0432\u044a\u0440\u0448\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 Silicon Labs Multiprotocol. \u0422\u043e\u0432\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u043e\u0442\u043d\u0435\u043c\u0435 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u043c\u0438\u043d\u0443\u0442\u0438.", + "start_addon": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0447\u0430\u043a\u0430\u0439\u0442\u0435, \u0434\u043e\u043a\u0430\u0442\u043e \u0437\u0430\u0432\u044a\u0440\u0448\u0438 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 Silicon Labs Multiprotocol. \u0422\u043e\u0432\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u043e\u0442\u043d\u0435\u043c\u0435 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434\u0438." + }, + "step": { + "addon_installed_other_device": { + "title": "\u041c\u043d\u043e\u0433\u043e\u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043d\u0430\u0442\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u0432\u0435\u0447\u0435 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430 \u0437\u0430 \u0434\u0440\u0443\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043c\u043d\u043e\u0433\u043e\u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043d\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430" + }, + "title": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043c\u0443\u043b\u0442\u0438\u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043d\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 IEEE 802.15.4 \u0440\u0430\u0434\u0438\u043e\u0442\u043e" + }, + "install_addon": { + "title": "\u0418\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Silicon Labs Multiprotocol \u0437\u0430\u043f\u043e\u0447\u043d\u0430" + }, + "show_revert_guide": { + "title": "\u041c\u043d\u043e\u0433\u043e\u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043d\u0430\u0442\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430 \u0437\u0430 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "start_addon": { + "title": "\u0414\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 Silicon Labs Multiprotocol \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/ca.json b/homeassistant/components/homeassistant_yellow/translations/ca.json new file mode 100644 index 00000000000..c7fdf1a4acd --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/ca.json @@ -0,0 +1,20 @@ +{ + "options": { + "abort": { + "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Silicon Labs Multiprotocol.", + "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Silicon Labs Multiprotocol-", + "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Silicon Labs Multiprotocol.", + "addon_start_failed": "No s'ha pogut iniciar el complement Silicon Labs Multiprotocol.", + "not_hassio": "Les opcions de maquinari nom\u00e9s es poden configurar a les instal\u00b7lacions HassOS.", + "zha_migration_failed": "La migraci\u00f3 ZHA no ha tingut \u00e8xit." + }, + "error": { + "unknown": "Error inesperat" + }, + "step": { + "start_addon": { + "title": "El complement Silicon Labs Multiprotocol s'est\u00e0 iniciant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/cs.json b/homeassistant/components/homeassistant_yellow/translations/cs.json new file mode 100644 index 00000000000..76899070cb1 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/cs.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Nepoda\u0159ilo se z\u00edskat informace o dopl\u0148ku Silicon Labs Multiprotocol.", + "addon_install_failed": "Nepoda\u0159ilo se nainstalovat dopln\u011bk Silicon Labs Multiprotocol.", + "addon_set_config_failed": "Nepoda\u0159ilo se nastavit konfiguraci Silicon Labs Multiprotocol.", + "addon_start_failed": "Spu\u0161t\u011bn\u00ed dopl\u0148ku Silicon Labs Multiprotocol se nezda\u0159ilo.", + "not_hassio": "Mo\u017enosti hardwaru lze konfigurovat pouze v instalac\u00edch HassOS.", + "zha_migration_failed": "Migrace ZHA se nezda\u0159ila." + }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "progress": { + "install_addon": "Po\u010dkejte pros\u00edm na dokon\u010den\u00ed instalace dopl\u0148ku Silicon Labs Multiprotocol. To m\u016f\u017ee trvat n\u011bkolik minut.", + "start_addon": "Po\u010dkejte pros\u00edm, ne\u017e se dokon\u010d\u00ed spu\u0161t\u011bn\u00ed dopl\u0148ku Silicon Labs Multiprotocol. To m\u016f\u017ee trvat n\u011bkolik sekund." + }, + "step": { + "addon_installed_other_device": { + "title": "Podpora v\u00edce protokol\u016f je ji\u017e povolena pro jin\u00e9 za\u0159\u00edzen\u00ed" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Povolit podporu v\u00edce protokol\u016f" + }, + "description": "Je-li povolena podpora v\u00edce protokol\u016f, lze r\u00e1dio IEEE 802.15.4 Home Assistant Yellow pou\u017e\u00edvat sou\u010dasn\u011b pro Zigbee i Thread (pou\u017e\u00edv\u00e1 Matter). Pokud je r\u00e1dio ji\u017e pou\u017e\u00edv\u00e1no integrac\u00ed ZHA Zigbee, bude ZHA p\u0159ekonfigurov\u00e1no pro pou\u017eit\u00ed multiprotokolov\u00e9ho firmwaru. \n\n Pozn\u00e1mka: Toto je experiment\u00e1ln\u00ed funkce.", + "title": "Povolit multiprotokolovou podporu na r\u00e1diu IEEE 802.15.4" + }, + "install_addon": { + "title": "Instalace dopl\u0148ku Silicon Labs Multiprotocol byla zah\u00e1jena" + }, + "show_revert_guide": { + "description": "Pokud chcete zm\u011bnit firmware pouze na Zigbee, prove\u010fte pros\u00edm n\u00e1sleduj\u00edc\u00ed ru\u010dn\u00ed kroky: \n\n * Odstra\u0148te dopln\u011bk Silicon Labs Multiprotocol \n\n * Flashujte pouze firmware Zigbee, postupujte podle n\u00e1vodu na https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually. \n\n * P\u0159ekonfigurujte ZHA pro migraci nastaven\u00ed do p\u0159eflashovan\u00e9ho r\u00e1dia", + "title": "Pro toto za\u0159\u00edzen\u00ed je povolena podpora v\u00edce protokol\u016f" + }, + "start_addon": { + "title": "Spou\u0161t\u00ed se dopln\u011bk Silicon Labs Multiprotocol." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/de.json b/homeassistant/components/homeassistant_yellow/translations/de.json new file mode 100644 index 00000000000..0b406d0f486 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/de.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Silicon Labs Multiprotokoll-Zusatzinformationen konnten nicht abgerufen werden.", + "addon_install_failed": "Die Installation des Silicon Labs Multiprotokoll Add-Ons ist fehlgeschlagen.", + "addon_set_config_failed": "Die Silicon Labs Multiprotokoll Konfiguration konnte nicht eingestellt werden.", + "addon_start_failed": "Das Silicon Labs Multiprotokoll Add-On konnte nicht gestartet werden.", + "not_hassio": "Die Hardwareoptionen k\u00f6nnen nur auf HassOS-Installationen konfiguriert werden.", + "zha_migration_failed": "Die ZHA Migration war nicht erfolgreich." + }, + "error": { + "unknown": "Unerwarteter Fehler" + }, + "progress": { + "install_addon": "Bitte warte, bis die Installation des Silicon Labs Multiprotokoll Add-ons abgeschlossen ist. Dies kann einige Minuten dauern.", + "start_addon": "Bitte warte, bis der Start des Silicon Labs Multiprotokoll Add-Ons abgeschlossen ist. Dies kann einige Sekunden dauern." + }, + "step": { + "addon_installed_other_device": { + "title": "Die Multiprotokoll Unterst\u00fctzung ist bereits f\u00fcr ein anderes Ger\u00e4t aktiviert" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Multiprotokoll Unterst\u00fctzung aktivieren" + }, + "description": "Wenn die Multiprotokoll Unterst\u00fctzung aktiviert ist, kann das IEEE 802.15.4-Radio des Home Assistant Yellow gleichzeitig f\u00fcr Zigbee und Thread (von Matter verwendet) verwendet werden. Wenn das Funkger\u00e4t bereits von der ZHA-Zigbee-Integration verwendet wird, wird ZHA neu konfiguriert, um die Multiprotokoll-Firmware zu verwenden. \n\nHinweis: Dies ist eine experimentelle Funktion.", + "title": "Aktiviere die Multiprotokoll Unterst\u00fctzung auf dem IEEE 802.15.4-Funkger\u00e4t" + }, + "install_addon": { + "title": "Die Installation des Silicon Labs Multiprotokoll Add-Ons hat begonnen" + }, + "show_revert_guide": { + "description": "Wenn du zu einer reinen Zigbee-Firmware wechseln m\u00f6chtest, f\u00fchre bitte die folgenden manuellen Schritte aus:\n\n * Entferne das Silicon Labs Multiprotokoll-Add-On\n\n * Flashe die reine Zigbee-Firmware, folge der Anleitung unter https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Rekonfiguriere ZHA, um die Einstellungen auf das neu geflashte Funkger\u00e4t zu migrieren.", + "title": "Multiprotokoll Unterst\u00fctzung ist f\u00fcr dieses Ger\u00e4t aktiviert" + }, + "start_addon": { + "title": "Das Silicon Labs Multiprotokoll Add-on wird gestartet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/el.json b/homeassistant/components/homeassistant_yellow/translations/el.json new file mode 100644 index 00000000000..8652b4e8a98 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/el.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol.", + "addon_install_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol.", + "addon_set_config_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd Silicon Labs.", + "addon_start_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol.", + "not_hassio": "\u039f\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03bf\u03cd\u03bd \u03bc\u03cc\u03bd\u03bf \u03c3\u03b5 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 HassOS.", + "zha_migration_failed": "\u0397 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 ZHA \u03b4\u03b5\u03bd \u03c0\u03ad\u03c4\u03c5\u03c7\u03b5." + }, + "error": { + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "progress": { + "install_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03b1\u03c1\u03ba\u03b5\u03c4\u03ac \u03bb\u03b5\u03c0\u03c4\u03ac.", + "start_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03bc\u03b5\u03c1\u03b9\u03ba\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1." + }, + "step": { + "addon_installed_other_device": { + "title": "\u0397 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b3\u03b9\u03b1 \u03bc\u03b9\u03b1 \u03ac\u03bb\u03bb\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7\u03c2 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd" + }, + "description": "\u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd, \u03b7 \u03c1\u03b1\u03b4\u03b9\u03bf\u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 IEEE 802.15.4 \u03c4\u03bf\u03c5 Home Assistant Yellow \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c4\u03b1\u03c5\u03c4\u03cc\u03c7\u03c1\u03bf\u03bd\u03b1 \u03c4\u03cc\u03c3\u03bf \u03b3\u03b9\u03b1 \u03c4\u03bf Zigbee \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf Thread (\u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Matter). \u03a3\u03b7\u03bc\u03b5\u03af\u03c9\u03c3\u03b7: \u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bc\u03b9\u03b1 \u03c0\u03b5\u03b9\u03c1\u03b1\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.", + "title": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7\u03c2 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd \u03c3\u03c4\u03bf\u03bd \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf IEEE 802.15.4" + }, + "install_addon": { + "title": "\u0397 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol \u03ad\u03c7\u03b5\u03b9 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03b9" + }, + "show_revert_guide": { + "description": "\u0395\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03bc\u03cc\u03bd\u03bf Zigbee, \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: \n\n * \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Silicon Labs Multiprotocol \n\n * \u0391\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bc\u03cc\u03bd\u03bf \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc Zigbee, \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03bf\u03b4\u03b7\u03b3\u03cc \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manual. \n\n * \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf ZHA \u03b3\u03b9\u03b1 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c3\u03c4\u03bf \u03b1\u03bd\u03b1\u03bd\u03b5\u03c9\u03bc\u03ad\u03bd\u03bf \u03c1\u03b1\u03b4\u03b9\u03cc\u03c6\u03c9\u03bd\u03bf", + "title": "\u0397 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "start_addon": { + "title": "\u039e\u03b5\u03ba\u03b9\u03bd\u03ac \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Silicon Labs Multiprotocol." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/en.json b/homeassistant/components/homeassistant_yellow/translations/en.json new file mode 100644 index 00000000000..8e12173f86a --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/en.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Failed to get Silicon Labs Multiprotocol add-on info.", + "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", + "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", + "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "not_hassio": "The hardware options can only be configured on HassOS installations.", + "zha_migration_failed": "The ZHA migration did not succeed." + }, + "error": { + "unknown": "Unexpected error" + }, + "progress": { + "install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Silicon Labs Multiprotocol add-on start completes. This may take some seconds." + }, + "step": { + "addon_installed_other_device": { + "title": "Multiprotocol support is already enabled for another device" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Enable multiprotocol support" + }, + "description": "When multiprotocol support is enabled, the {hardware_name}'s IEEE 802.15.4 radio can be used for both Zigbee and Thread (used by Matter) at the same time. If the radio is already used by the ZHA Zigbee integration, ZHA will be reconfigured to use the multiprotocol firmware.\n\nNote: This is an experimental feature.", + "title": "Enable multiprotocol support on the IEEE 802.15.4 radio" + }, + "install_addon": { + "title": "The Silicon Labs Multiprotocol add-on installation has started" + }, + "show_revert_guide": { + "description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio", + "title": "Multiprotocol support is enabled for this device" + }, + "start_addon": { + "title": "The Silicon Labs Multiprotocol add-on is starting." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/es.json b/homeassistant/components/homeassistant_yellow/translations/es.json new file mode 100644 index 00000000000..d93a329efd6 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/es.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Silicon Labs Multiprotocol.", + "addon_install_failed": "No se pudo instalar el complemento Silicon Labs Multiprotocol.", + "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de Silicon Labs Multiprotocol.", + "addon_start_failed": "No se pudo iniciar el complemento Silicon Labs Multiprotocol.", + "not_hassio": "Las opciones de hardware solo se pueden configurar en instalaciones de HassOS.", + "zha_migration_failed": "La migraci\u00f3n de ZHA no tuvo \u00e9xito." + }, + "error": { + "unknown": "Error inesperado" + }, + "progress": { + "install_addon": "Por favor, espera mientras finaliza la instalaci\u00f3n del complemento Silicon Labs Multiprotocol. Esto puede tardar varios minutos.", + "start_addon": "Por favor, espera mientras se completa el inicio del complemento Silicon Labs Multiprotocol. Esto puede tardar unos segundos." + }, + "step": { + "addon_installed_other_device": { + "title": "El soporte multiprotocolo ya est\u00e1 habilitado para otro dispositivo" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Habilitar soporte multiprotocolo" + }, + "description": "Cuando el soporte multiprotocolo est\u00e1 habilitado, la radio IEEE 802.15.4 de Home Assistant Yellow se puede usar tanto para Zigbee como para Thread (usado por Matter) al mismo tiempo. Si la integraci\u00f3n ZHA Zigbee ya usa la radio, ZHA se volver\u00e1 a configurar para usar el firmware multiprotocolo. \n\nNota: Esta es una caracter\u00edstica experimental.", + "title": "Habilitar el soporte multiprotocolo en la radio IEEE 802.15.4" + }, + "install_addon": { + "title": "La instalaci\u00f3n del complemento Silicon Labs Multiprotocol ha comenzado" + }, + "show_revert_guide": { + "description": "Si quieres cambiar el firmware a solo Zigbee, completa los siguientes pasos manuales: \n\n* Eliminar el complemento Silicon Labs Multiprotocol \n\n* Actualiza el firmware a solo Zigbee, sigue la gu\u00eda en https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually. \n\n* Vuelve a configurar ZHA para migrar la configuraci\u00f3n a la radio actualizada", + "title": "El soporte multiprotocolo est\u00e1 habilitado para este dispositivo" + }, + "start_addon": { + "title": "El complemento Silicon Labs Multiprotocol est\u00e1 iniciando." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/et.json b/homeassistant/components/homeassistant_yellow/translations/et.json new file mode 100644 index 00000000000..2f2203d1e3c --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/et.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Silicon Labs Multiprotocol add-on info saamine eba\u00f5nnestus.", + "addon_install_failed": "Silicon Labs Multiprotocol add-on'i paigaldamine eba\u00f5nnestus.", + "addon_set_config_failed": "Silicon Labs Multiprotocol konfiguratsiooni seadistamine eba\u00f5nnestus.", + "addon_start_failed": "Silicon Labsi mitmeprotokolli lisandmooduli k\u00e4ivitamine nurjus.", + "not_hassio": "Riistvaravalikuid saab konfigureerida ainult HassOS-i paigaldustes.", + "zha_migration_failed": "ZHA sidumise siirdamine nurjus." + }, + "error": { + "unknown": "Ootamatu t\u00f5rge" + }, + "progress": { + "install_addon": "Oota kuni Silicon Labsi mitmeprotokolli lisandmooduli installimine l\u00f5peb. Selleks v\u00f5ib kuluda mitu minutit.", + "start_addon": "Oota kuni Silicon Labsi mitmeprotokolli lisandmooduli k\u00e4ivitamine on l\u00f5pule viidud. See v\u00f5ib v\u00f5tta m\u00f5ne sekundi." + }, + "step": { + "addon_installed_other_device": { + "title": "Mitmeprotokolli tugi on m\u00f5ne teise seadme jaoks juba lubatud" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Luba multiprotokolli tugi" + }, + "description": "Kui mitme protokolli tugi on lubatud, saab Home Assistant Yellowi IEEE 802.15.4 raadiot kasutada samaaegselt nii Zigbee kui ka Threadi jaoks (kasutab Matter). Kui ZHA Zigbee integratsioon juba kasutab raadiot, konfigureeritakse ZHA \u00fcmber mitmeprotokollilise p\u00fcsivara kasutamiseks. \n\n M\u00e4rkus. See on eksperimentaalne funktsioon.", + "title": "Multiprotokollide toe lubamine IEEE 802.15.4 raadiosides" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol add-on paigaldus on alanud" + }, + "show_revert_guide": { + "description": "Kui soovid muuta ainult Zigbee p\u00fcsivara, tee j\u00e4rgmised sammud:\n\n * Eemalda Silicon Labsi mitmeprotokolli lisand \n\n * V\u00e4rskenda ainult Zigbee p\u00fcsivara, j\u00e4rgi juhendit aadressil https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually. \n\n * Seadistuste \u00fcleviimiseks v\u00e4rskendatud raadiosse seadista ZHA uuesti", + "title": "Multiprotokollide tugi on selle seadme puhul lubatud." + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol lisandmoodul k\u00e4ivitub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/fr.json b/homeassistant/components/homeassistant_yellow/translations/fr.json new file mode 100644 index 00000000000..b6aa5ffebd9 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/fr.json @@ -0,0 +1,22 @@ +{ + "options": { + "abort": { + "zha_migration_failed": "La migration ZHA n\u2019a pas r\u00e9ussi." + }, + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "install_addon": { + "title": "L'installation du module compl\u00e9mentaire multi-protocoles de Silicon Labs a commenc\u00e9." + }, + "show_revert_guide": { + "description": "Si vous voulez utiliser uniquement le firmware Zigbee, suivez les \u00e9tapes manuelles suivantes :\n\n * Supprimez le module compl\u00e9mentaire multi-protocoles de Silicon Labs.\n\n * Flashez le firmware qui supporte uniquement Zigbee en suivant le guide https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigurez ZHA pour migrer les param\u00e8tres vers le nouveau canal", + "title": "Le support multi-protocoles est activ\u00e9 pour cet appareil." + }, + "start_addon": { + "title": "Le module compl\u00e9mentaire multi-protocoles de Silicon Labs d\u00e9marre." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/hu.json b/homeassistant/components/homeassistant_yellow/translations/hu.json new file mode 100644 index 00000000000..b2fdffb1450 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/hu.json @@ -0,0 +1,40 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Nem siker\u00fclt lek\u00e9rni a Silicon Labs Multiprotocol kieg\u00e9sz\u00edt\u0151 adatait.", + "addon_install_failed": "Nem siker\u00fclt telep\u00edteni a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.", + "addon_set_config_failed": "A Silicon Labs Multiprotokoll konfigur\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa sikertelen volt.", + "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.", + "not_hassio": "A hardverbe\u00e1ll\u00edt\u00e1sok csak HassOS telep\u00edt\u00e9sekn\u00e9l konfigur\u00e1lhat\u00f3k." + }, + "error": { + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "progress": { + "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + }, + "step": { + "addon_installed_other_device": { + "title": "A multiprotokoll-t\u00e1mogat\u00e1s m\u00e1r enged\u00e9lyezve van egy m\u00e1sik eszk\u00f6z sz\u00e1m\u00e1ra" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyez\u00e9se" + }, + "description": "Ha a multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van, a Home Assistant Yellow IEEE 802.15.4 r\u00e1di\u00f3ja egyszerre haszn\u00e1lhat\u00f3 a Zigbee \u00e9s a Thread (a Matter \u00e1ltal haszn\u00e1lt) r\u00e1di\u00f3hoz. Megjegyz\u00e9s: Ez egy k\u00eds\u00e9rleti funkci\u00f3.", + "title": "Multiprotokoll t\u00e1mogat\u00e1s\u00e1nak enged\u00e9lyez\u00e9se az IEEE 802.15.4 r\u00e1di\u00f3ban" + }, + "install_addon": { + "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se folyamatban van" + }, + "show_revert_guide": { + "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, k\u00e9rj\u00fck, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", + "title": "A multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van az eszk\u00f6z\u00f6n." + }, + "start_addon": { + "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elind\u00edt\u00e1sa folyamatban van" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/id.json b/homeassistant/components/homeassistant_yellow/translations/id.json new file mode 100644 index 00000000000..e3237d7b95e --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/id.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Gagal mendapatkan info add-on Silicon Labs Multiprotocol.", + "addon_install_failed": "Gagal menginstal add-on Silicon Labs Multiprotocol.", + "addon_set_config_failed": "Gagal mengatur konfigurasi Silicon Labs Multiprotocol.", + "addon_start_failed": "Gagal memulai add-on Silicon Labs Multiprotocol.", + "not_hassio": "Opsi perangkat keras hanya bisa dikonfigurasi pada instalasi HassOS.", + "zha_migration_failed": "Migrasi ZHA tidak berhasil." + }, + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + }, + "progress": { + "install_addon": "Harap tunggu hingga penginstalan add-on Silicon Labs Multiprotocol selesai. Ini bisa memakan waktu beberapa saat.", + "start_addon": "Harap tunggu hingga add-on Silicon Labs Multiprotocol selesai. Ini mungkin perlu waktu beberapa saat." + }, + "step": { + "addon_installed_other_device": { + "title": "Dukungan multiprotokol sudah diaktifkan untuk perangkat lain" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Aktifkan dukungan multiprotokol" + }, + "description": "Jika dukungan multiprotocol diaktifkan, radio IEEE 802.15.4 Home Assistant Yellow dapat digunakan untuk Zigbee dan Thread (digunakan oleh Matter) secara bersamaan. Catatan: Ini adalah fitur eksperimental. Jika komponen radio telah digunakan oleh integrasi ZHA Zigbee, ZHA akan dikonfigurasi ulang untuk menggunakan firmware multiprotokol.\n\nCatatan: Fitur ini bersifat eksperimental.", + "title": "Aktifkan dukungan multiprotokol pada radio IEEE 802.15.4" + }, + "install_addon": { + "title": "Penginstalan add-on Multiprotocol Silicon Labs telah dimulai" + }, + "show_revert_guide": { + "description": "Jika Anda ingin mengubah ke firmware Zigbee saja, selesaikan langkah-langkah manual berikut ini:\n\n * Hapus add-on Multiprotocol Silicon Labs\n\n * Flash firmware khusus Zigbee, ikuti panduan di https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Konfigurasikan ulang ZHA untuk memigrasikan pengaturan ke radio yang diflash ulang", + "title": "Dukungan multiprotokol diaktifkan untuk perangkat ini" + }, + "start_addon": { + "title": "Add-on Multiprotokol Silicon Labs sedang dimulai." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/it.json b/homeassistant/components/homeassistant_yellow/translations/it.json new file mode 100644 index 00000000000..556d0cd3737 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/it.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Impossibile ottenere informazioni sul componente aggiuntivo Silicon Labs Multiprotocol.", + "addon_install_failed": "Impossibile installare il componente aggiuntivo Silicon Labs Multiprotocol.", + "addon_set_config_failed": "Impossibile impostare la configurazione di Silicon Labs Multiprotocol.", + "addon_start_failed": "Impossibile avviare il componente aggiuntivo Silicon Labs Multiprotocol.", + "not_hassio": "Le opzioni hardware possono essere configurate solo su installazioni HassOS.", + "zha_migration_failed": "La migrazione ZHA non \u00e8 riuscita." + }, + "error": { + "unknown": "Errore imprevisto" + }, + "progress": { + "install_addon": "Attendere il completamento dell'installazione del componente aggiuntivo Silicon Labs Multiprotocol. Questo pu\u00f2 richiedere diversi minuti.", + "start_addon": "Attendi il completamento dell'avvio del componente aggiuntivo Silicon Labs Multiprotocol. Questo potrebbe richiedere alcuni secondi." + }, + "step": { + "addon_installed_other_device": { + "title": "Il supporto multiprotocollo \u00e8 gi\u00e0 abilitato per un altro dispositivo" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Abilita il supporto multiprotocollo" + }, + "description": "Quando il supporto multiprotocollo \u00e8 abilitato, la radio IEEE 802.15.4 di Home Assistant Yellow pu\u00f2 essere utilizzata contemporaneamente sia per Zigbee che per Thread (utilizzato da Matter). Nota: questa \u00e8 una funzione sperimentale.", + "title": "Abilita il supporto multiprotocollo sulla radio IEEE 802.15.4" + }, + "install_addon": { + "title": "L'installazione del componente aggiuntivo Silicon Labs Multiprotocol \u00e8 iniziata" + }, + "show_revert_guide": { + "description": "Se desideri passare al solo firmware Zigbee, completa i seguenti passaggi manuali: \n\n * Rimuovi il componente aggiuntivo Silicon Labs Multiprotocol \n\n * Caricare solo il firmware Zigbee, segui la guida su https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually. \n\n * Riconfigurare ZHA per migrare le impostazioni alla radio ricaricata", + "title": "Il supporto multiprotocollo \u00e8 abilitato per questo dispositivo" + }, + "start_addon": { + "title": "Il componente aggiuntivo Silicon Labs Multiprotocol sta per essere avviato." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/nl.json b/homeassistant/components/homeassistant_yellow/translations/nl.json new file mode 100644 index 00000000000..ee6cec8c81c --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/nl.json @@ -0,0 +1,40 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Kan geen add-on informatie opvragen voor de Silicon Labs Multiprotocol add-on.", + "addon_install_failed": "Het installeren van de SIlicon Labs Multiprotocol add-on is mislukt.", + "addon_set_config_failed": "Het instellen van de Silicon Labs Multiprotocol configuratie is mislukt.", + "addon_start_failed": "Het opstarten van de Silicon Labs Multiprotocol add-on is mislukt.", + "not_hassio": "De hardware opties kunnen alleen worden ingesteld voor HassOS installaties.", + "zha_migration_failed": "De ZHA migratie is niet gelukt." + }, + "error": { + "unknown": "Onverwachte fout" + }, + "progress": { + "install_addon": "Wacht even tot de Silicon Labs Multiprotocol add-on installatie is voltooid. Dit kan enige minuten duren.", + "start_addon": "Wacht even tot de Silicon Labs Multiprotocol add-on is opgestart. Dit kan enige seconden duren." + }, + "step": { + "addon_installed_other_device": { + "title": "Multi-protocol ondersteuning is al voor een ander apparaat ingeschakeld" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Inschakelen multi-protocol ondersteuning" + }, + "description": "Wanneer multi-protocol ondersteuning is ingeschakeld, zal het Home Assistant Yellow IEEE 802.15.4 toegangspunt gelijktijdig voor zowel Zigbee als Thread (gebruikt door Matter) ingezet worden. Als het toegangspunt als wordt gebruikt door de ZHA Zigbee integratie, dan zal, ZHA worden be ge-reconfigureerd om de multi-protocol firmware te gaan gebruiken.\n\nOpmerking: Dit is een experimentele functie.", + "title": "Inschakelen multi-protocol ondersteuning op het IEEE 802.15.4 toegangspunt" + }, + "install_addon": { + "title": "De Silicon Labs Multiprotocol add-on is gestart" + }, + "show_revert_guide": { + "description": "Als je naar Zigbee-only firmware wilt overstappen, voltooi dan de volgende handmatig stappen:\n\n * Verwijderd de Silicon Labs Multiprotocol addon\n\n * Flash de Zigbee-only firmware, volg de aanwijzigingen op https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually\n\n * Herconfigureer ZHA om de instellingen te migreren naar het ge-reflashed toegangspunt" + }, + "start_addon": { + "title": "De Silicon Labs Multiprotocol add-on is aan het opstarten." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/no.json b/homeassistant/components/homeassistant_yellow/translations/no.json new file mode 100644 index 00000000000..d93b078dd4b --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/no.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Kunne ikke hente informasjon om tilleggsprogrammet for Silicon Labs Multiprotocol.", + "addon_install_failed": "Kunne ikke installere Silicon Labs Multiprotocol-tillegget.", + "addon_set_config_failed": "Kunne ikke angi Silicon Labs Multiprotocol-konfigurasjon.", + "addon_start_failed": "Kunne ikke starte Silicon Labs Multiprotocol-tillegget.", + "not_hassio": "Maskinvarealternativene kan bare konfigureres p\u00e5 HassOS-installasjoner.", + "zha_migration_failed": "ZHA-migreringen lyktes ikke." + }, + "error": { + "unknown": "Uventet feil" + }, + "progress": { + "install_addon": "Vennligst vent mens installasjonen av Silicon Labs Multiprotocol-tillegget fullf\u00f8res. Dette kan ta flere minutter.", + "start_addon": "Vennligst vent mens oppstarten av Silicon Labs Multiprotocol-tillegget fullf\u00f8res. Dette kan ta noen sekunder." + }, + "step": { + "addon_installed_other_device": { + "title": "Multiprotokollst\u00f8tte er allerede aktivert for en annen enhet" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Aktiver st\u00f8tte for multiprotokoll" + }, + "description": "N\u00e5r multiprotokollst\u00f8tte er aktivert, kan Home Assistant Yellows IEEE 802.15.4-radio brukes for b\u00e5de Zigbee og Thread (brukt av Matter) samtidig. Hvis radioen allerede brukes av ZHA Zigbee-integrasjonen, vil ZHA bli rekonfigurert til \u00e5 bruke multiprotokollfastvaren. \n\n Merk: Dette er en eksperimentell funksjon.", + "title": "Aktiver st\u00f8tte for multiprotokoll p\u00e5 IEEE 802.15.4-radioen" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol-tilleggsinstallasjonen har startet" + }, + "show_revert_guide": { + "description": "Hvis du vil bytte til kun Zigbee-firmware, m\u00e5 du fullf\u00f8re f\u00f8lgende manuelle trinn: \n\n * Fjern Silicon Labs Multiprotocol-tillegget \n\n * Flash den eneste Zigbee-fastvaren, f\u00f8lg veiledningen p\u00e5 https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually. \n\n * Konfigurer ZHA p\u00e5 nytt for \u00e5 migrere innstillinger til radioen med oppdatering", + "title": "Multiprotokollst\u00f8tte er aktivert for denne enheten" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol-tillegget starter." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/pl.json b/homeassistant/components/homeassistant_yellow/translations/pl.json new file mode 100644 index 00000000000..a0c4c902730 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/pl.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Nie uda\u0142o si\u0119 pobra\u0107 informacji o dodatku Silicon Labs Multiprotocol.", + "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Silicon Labs Multiprotocol.", + "addon_set_config_failed": "Nie uda\u0142o si\u0119 ustawi\u0107 konfiguracji Silicon Labs Multiprotocol.", + "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Silicon Labs Multiprotocol.", + "not_hassio": "Opcje sprz\u0119towe mo\u017cna skonfigurowa\u0107 tylko w instalacjach HassOS.", + "zha_migration_failed": "Migracja ZHA nie powiod\u0142a si\u0119." + }, + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "progress": { + "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Silicon Labs Multiprotocol. Mo\u017ce to potrwa\u0107 kilka minut.", + "start_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 uruchamianie dodatku Silicon Labs Multiprotocol. Mo\u017ce to potrwa\u0107 kilka sekund." + }, + "step": { + "addon_installed_other_device": { + "title": "Obs\u0142uga Multiprotocol jest ju\u017c w\u0142\u0105czona dla innego urz\u0105dzenia" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "W\u0142\u0105cz obs\u0142ug\u0119 Multiprotocol" + }, + "description": "Gdy w\u0142\u0105czona jest obs\u0142uga Multiprotocol, radio IEEE 802.15.4 Home Assistant Yellow mo\u017ce by\u0107 u\u017cywane jednocze\u015bnie dla Zigbee i Thread (u\u017cywane przez Matter). Uwaga: jest to funkcja eksperymentalna. Je\u015bli radio jest ju\u017c u\u017cywane przez integracj\u0119 ZHA Zigbee, ZHA zostanie ponownie skonfigurowane, aby korzysta\u0107 z oprogramowania multiprotocol.\n\nUwaga: jest to funkcja eksperymentalna.", + "title": "W\u0142\u0105cz obs\u0142ug\u0119 multiprotocol w radiu IEEE 802.15.4" + }, + "install_addon": { + "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku Silicon Labs Multiprotocol" + }, + "show_revert_guide": { + "description": "Je\u015bli chcesz zmieni\u0107 oprogramowanie obs\u0142uguj\u0105ce tylko Zigbee, wykonaj nast\u0119puj\u0105ce czynno\u015bci r\u0119czne: \n\n* Usu\u0144 dodatek Silicon Labs Multiprotocol \n\n* Wgraj oprogramowanie tylko dla Zigbee, post\u0119puj zgodnie z instrukcjami na stronie https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually. \n\n* Ponownie skonfiguruj ZHA, aby przeprowadzi\u0107 migracj\u0119 ustawie\u0144 do przeprogramowanego radia", + "title": "Obs\u0142uga multiprotocol jest w\u0142\u0105czona dla tego urz\u0105dzenia" + }, + "start_addon": { + "title": "Uruchamianie dodatku Silicon Labs Multiprotocol." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/pt-BR.json b/homeassistant/components/homeassistant_yellow/translations/pt-BR.json new file mode 100644 index 00000000000..b828d7a9d5b --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Falha ao obter informa\u00e7\u00f5es do add-on Silicon Labs Multiprotocol.", + "addon_install_failed": "Falha ao instalar o add-on Silicon Labs Multiprotocol.", + "addon_set_config_failed": "Falha ao definir a configura\u00e7\u00e3o multiprotocolo da Silicon Labs.", + "addon_start_failed": "Falha ao iniciar o add-on Silicon Labs Multiprotocol.", + "not_hassio": "As op\u00e7\u00f5es de hardware s\u00f3 podem ser configuradas em instala\u00e7\u00f5es HassOS.", + "zha_migration_failed": "A migra\u00e7\u00e3o ZHA n\u00e3o foi bem-sucedida." + }, + "error": { + "unknown": "Erro inesperado" + }, + "progress": { + "install_addon": "Aguarde enquanto a instala\u00e7\u00e3o do add-on Silicon Labs Multiprotocol \u00e9 conclu\u00edda. Isso pode levar v\u00e1rios minutos.", + "start_addon": "Aguarde enquanto a inicializa\u00e7\u00e3o do add-on Silicon Labs Multiprotocol \u00e9 conclu\u00edda. Isso pode levar alguns segundos." + }, + "step": { + "addon_installed_other_device": { + "title": "O suporte multiprotocolo j\u00e1 est\u00e1 ativado para outro dispositivo" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "Habilitar suporte multiprotocolo" + }, + "description": "Quando o suporte multiprotocolo est\u00e1 ativado, o r\u00e1dio IEEE 802.15.4 do Home Assistant Yellow pode ser usado para Zigbee e Thread (usado por Matter) ao mesmo tempo. Se o r\u00e1dio j\u00e1 estiver sendo usado pela integra\u00e7\u00e3o ZHA Zigbee, o ZHA ser\u00e1 reconfigurado para usar o firmware multiprotocolo. \n\n Nota: Este \u00e9 um recurso experimental.", + "title": "Habilitar o suporte multiprotocolo no r\u00e1dio IEEE 802.15.4" + }, + "install_addon": { + "title": "A instala\u00e7\u00e3o do add-on Silicon Labs Multiprotocol foi iniciada" + }, + "show_revert_guide": { + "description": "Se voc\u00ea deseja alterar para o firmware somente Zigbee, conclua as seguintes etapas manuais: \n\n * Remova o add-on Silicon Labs Multiprotocol \n\n * Atualize apenas o firmware Zigbee, siga o guia em https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually. \n\n * Reconfigure o ZHA para migrar as configura\u00e7\u00f5es para o r\u00e1dio reflashed", + "title": "O suporte multiprotocolo est\u00e1 ativado para este dispositivo" + }, + "start_addon": { + "title": "O add-on Silicon Labs Multiprotocol est\u00e1 iniciando." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/ru.json b/homeassistant/components/homeassistant_yellow/translations/ru.json new file mode 100644 index 00000000000..f1ae0759e15 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/ru.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "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 \"Silicon Labs Multiprotocol\".", + "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 \"Silicon Labs Multiprotocol\".", + "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 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \"Silicon Labs Multiprotocol\".", + "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 \"Silicon Labs Multiprotocol\".", + "not_hassio": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0445 HassOS.", + "zha_migration_failed": "\u041c\u0438\u0433\u0440\u0430\u0446\u0438\u044f ZHA \u043d\u0435 \u0443\u0434\u0430\u043b\u0430\u0441\u044c." + }, + "error": { + "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 \"Silicon Labs Multiprotocol\". \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.", + "start_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \"Silicon Labs Multiprotocol\". \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434." + }, + "step": { + "addon_installed_other_device": { + "title": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432 \u0443\u0436\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u043b\u044f \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432" + }, + "description": "\u041a\u043e\u0433\u0434\u0430 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432, \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c Home Assistant Yellow \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0430 IEEE 802.15.4 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043a\u0430\u043a \u0434\u043b\u044f Zigbee, \u0442\u0430\u043a \u0438 \u0434\u043b\u044f Thread (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 Matter). \u0415\u0441\u043b\u0438 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0432 ZHA \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 Zigbee, ZHA \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0434\u043b\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432.\n\n\u041f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0435: \u044d\u0442\u043e \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f.", + "title": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432 \u043d\u0430 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u0435 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0430 IEEE 802.15.4." + }, + "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 \"Silicon Labs Multiprotocol\"" + }, + "show_revert_guide": { + "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0449\u0443\u044e \u0442\u043e\u043b\u044c\u043a\u043e Zigbee, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f:\n\n* \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"Silicon Labs Multiprotocol\".\n\n* \u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 Zigbee, \u0441\u043b\u0435\u0434\u0443\u044f \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0443 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n* \u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 ZHA \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u043d\u0430 \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u043e\u0448\u0438\u0442\u044b\u0439 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c.", + "title": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432" + }, + "start_addon": { + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Silicon Labs Multiprotocol \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/sk.json b/homeassistant/components/homeassistant_yellow/translations/sk.json new file mode 100644 index 00000000000..f40c05f8068 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/sk.json @@ -0,0 +1,12 @@ +{ + "options": { + "error": { + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "addon_installed_other_device": { + "title": "Podpora viacer\u00fdch protokolov je u\u017e povolen\u00e1 pre in\u00e9 zariadenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/zh-Hans.json b/homeassistant/components/homeassistant_yellow/translations/zh-Hans.json new file mode 100644 index 00000000000..af82bdcca0a --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/zh-Hans.json @@ -0,0 +1,10 @@ +{ + "options": { + "abort": { + "zha_migration_failed": "ZHA \u8fc1\u79fb\u672a\u6210\u529f\u3002" + }, + "error": { + "unknown": "\u610f\u6599\u4e4b\u5916\u7684\u9519\u8bef" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/zh-Hant.json b/homeassistant/components/homeassistant_yellow/translations/zh-Hant.json new file mode 100644 index 00000000000..bab420862dc --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/zh-Hant.json @@ -0,0 +1,41 @@ +{ + "options": { + "abort": { + "addon_info_failed": "\u53d6\u5f97 Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_install_failed": "\u5b89\u88dd Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u5931\u6557\u3002", + "addon_set_config_failed": "\u8a2d\u5b9a Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u5931\u6557\u3002", + "addon_start_failed": "\u555f\u52d5 Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u5931\u6557\u3002", + "not_hassio": "\u786c\u9ad4\u9078\u9805\u50c5\u80fd\u65bc HassOS \u5b89\u88dd\u6a21\u5f0f\u9032\u884c\u8a2d\u5b9a\u3002", + "zha_migration_failed": "ZHA \u9077\u79fb\u672a\u6210\u529f\u3002" + }, + "error": { + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "progress": { + "install_addon": "\u8acb\u7a0d\u7b49 Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\u3002\u53ef\u80fd\u9700\u8981\u5e7e\u5206\u9418\u3002", + "start_addon": "\u8acb\u7a0d\u7b49 Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + }, + "step": { + "addon_installed_other_device": { + "title": "Multiprotocol \u652f\u63f4\u5df2\u7d93\u65bc\u5176\u4ed6\u88dd\u7f6e\u958b\u555f" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u555f\u7528 Multiprotocol \u652f\u63f4" + }, + "description": "\u7576\u555f\u7528 Multiprotocol \u652f\u63f4\u6642\u3001Home Assistant Yellow \u7684 IEEE 802.15.4 radio \u53ef\u540c\u6642\u4f5c\u70ba Zigbee \u8207 Thread \uff08\u7528\u65bc Matter\uff09\u4f7f\u7528\u3002\u5047\u5982 Radio \u5df2\u7d93\u88ab ZHA Zigbee \u6574\u5408\u6240\u4f7f\u7528\u3001ZHA \u5c07\u6703\u9032\u884c\u91cd\u65b0\u8a2d\u5b9a\u4ee5\u4f7f\u7528 Multiprotocol \u97cc\u9ad4\u3002\n\n\u6ce8\u610f\uff1a\u76ee\u524d\u70ba\u5be6\u9a57\u6027\u529f\u80fd\u3002", + "title": "\u65bc IEEE 802.15.4 radio \u4e0a\u555f\u7528 Multiprotocol \u652f\u63f4" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u5df2\u555f\u7528\u3002" + }, + "show_revert_guide": { + "description": "\u5047\u5982\u60f3\u8b8a\u66f4\u70ba\u50c5 Zigbee \u97cc\u9ad4\u3001\u8acb\u5b8c\u6210\u4ee5\u4e0b\u624b\u52d5\u6b65\u9a5f\uff1a\n\n * \u79fb\u9664 Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\n\n * \u5237\u5165\u50c5 Zigbee \u97cc\u9ad4\uff0c\u8acb\u53c3\u95b1\u64cd\u4f5c\u6307\u5f15 https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually\u3002\n\n * \u91cd\u65b0\u8a2d\u5b9a ZHA \u4ee5\u9077\u79fb\u8a2d\u5b9a\u81f3\u91cd\u65b0\u5237\u5165\u7684 Radio", + "title": "\u88dd\u7f6e\u4e4b Multiprotocol \u652f\u63f4\u5df2\u958b\u555f" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol \u9644\u52a0\u5143\u4ef6\u555f\u7528\u4e2d\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index aca53a50105..eef43892677 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -200,6 +200,10 @@ def get_accessory( # noqa: C901 or SensorDeviceClass.PM25 in state.entity_id ): a_type = "PM25Sensor" + elif device_class == SensorDeviceClass.NITROGEN_DIOXIDE: + a_type = "NitrogenDioxideSensor" + elif device_class == SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: + a_type = "VolatileOrganicCompoundsSensor" elif ( device_class == SensorDeviceClass.GAS or SensorDeviceClass.GAS in state.entity_id @@ -354,12 +358,12 @@ class HomeAccessory(Accessory): # type: ignore[misc] if state is not None: battery_found = state.state else: - self.linked_battery_sensor = None _LOGGER.warning( "%s: Battery sensor state missing: %s", self.entity_id, self.linked_battery_sensor, ) + self.linked_battery_sensor = None if not battery_found: return diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 264801c521f..58e1e13a3f3 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -196,6 +196,7 @@ CHAR_MODEL = "Model" CHAR_MOTION_DETECTED = "MotionDetected" CHAR_MUTE = "Mute" CHAR_NAME = "Name" +CHAR_NITROGEN_DIOXIDE_DENSITY = "NitrogenDioxideDensity" CHAR_OBSTRUCTION_DETECTED = "ObstructionDetected" CHAR_OCCUPANCY_DETECTED = "OccupancyDetected" CHAR_ON = "On" @@ -226,6 +227,7 @@ CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle" CHAR_HOLD_POSITION = "HoldPosition" CHAR_TEMP_DISPLAY_UNITS = "TemperatureDisplayUnits" CHAR_VALVE_TYPE = "ValveType" +CHAR_VOC_DENSITY = "VOCDensity" CHAR_VOLUME = "Volume" CHAR_VOLUME_SELECTOR = "VolumeSelector" CHAR_VOLUME_CONTROL_TYPE = "VolumeControlType" diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index d145d826337..5fa4b01b85b 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -44,14 +44,14 @@ "data": { "entities": "Entit\u00e4ten" }, - "description": "Alle \"{domains}\"-Entit\u00e4ten werden einbezogen, mit Ausnahme der ausgeschlossenen Entit\u00e4ten und kategorisierten Entit\u00e4ten.", + "description": "Alle \"{domains}\" Entit\u00e4ten werden einbezogen, mit Ausnahme der ausgeschlossenen Entit\u00e4ten und kategorisierten Entit\u00e4ten.", "title": "W\u00e4hle die auszuschlie\u00dfenden Einheiten aus" }, "include": { "data": { "entities": "Entit\u00e4ten" }, - "description": "Alle \"{domains}\"-Entit\u00e4ten werden einbezogen, sofern nicht bestimmte Entit\u00e4ten ausgew\u00e4hlt werden.", + "description": "Alle \"{domains}\" Entit\u00e4ten werden einbezogen, sofern nicht bestimmte Entit\u00e4ten ausgew\u00e4hlt werden.", "title": "W\u00e4hle die einzuschlie\u00dfenden Entit\u00e4ten aus" }, "init": { diff --git a/homeassistant/components/homekit/translations/sk.json b/homeassistant/components/homekit/translations/sk.json new file mode 100644 index 00000000000..4469563f2f3 --- /dev/null +++ b/homeassistant/components/homekit/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "pairing": { + "description": "Na dokon\u010denie p\u00e1rovania postupujte pod\u013ea pokynov v \u010dasti \u201eUpozornenia\u201c v \u010dasti \u201eP\u00e1rovanie HomeKit\u201c." + } + } + }, + "options": { + "step": { + "accessory": { + "data": { + "entities": "Entita" + } + }, + "advanced": { + "title": "Roz\u0161\u00edren\u00e1 konfigur\u00e1cia" + }, + "exclude": { + "data": { + "entities": "Entity" + } + }, + "include": { + "data": { + "entities": "Entity" + } + }, + "yaml": { + "description": "T\u00e1to polo\u017eka sa ovl\u00e1da pomocou YAML" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index e877ffff07a..4e9c897dff9 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -33,10 +33,12 @@ from .const import ( CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, CHAR_MOTION_DETECTED, + CHAR_NITROGEN_DIOXIDE_DENSITY, CHAR_OCCUPANCY_DETECTED, CHAR_PM10_DENSITY, CHAR_PM25_DENSITY, CHAR_SMOKE_DETECTED, + CHAR_VOC_DENSITY, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, SERV_CARBON_DIOXIDE_SENSOR, @@ -55,7 +57,9 @@ from .const import ( from .util import ( convert_to_float, density_to_air_quality, + density_to_air_quality_nitrogen_dioxide, density_to_air_quality_pm10, + density_to_air_quality_voc, temperature_to_homekit, ) @@ -206,7 +210,7 @@ class PM10Sensor(AirQualitySensor): def async_update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if not density: + if density is None: return if self.char_density.value != density: self.char_density.set_value(density) @@ -233,7 +237,7 @@ class PM25Sensor(AirQualitySensor): def async_update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if not density: + if density is None: return if self.char_density.value != density: self.char_density.set_value(density) @@ -244,6 +248,62 @@ class PM25Sensor(AirQualitySensor): _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) +@TYPES.register("NitrogenDioxideSensor") +class NitrogenDioxideSensor(AirQualitySensor): + """Generate a NitrogenDioxideSensor accessory as NO2 sensor.""" + + def create_services(self): + """Override the init function for PM 2.5 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_NITROGEN_DIOXIDE_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char( + CHAR_NITROGEN_DIOXIDE_DENSITY, value=0 + ) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if density is None: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_nitrogen_dioxide(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + +@TYPES.register("VolatileOrganicCompoundsSensor") +class VolatileOrganicCompoundsSensor(AirQualitySensor): + """Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor.""" + + def create_services(self): + """Override the init function for PM 2.5 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_VOC_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_VOC_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if density is None: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_voc(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + @TYPES.register("CarbonMonoxideSensor") class CarbonMonoxideSensor(HomeAccessory): """Generate a CarbonMonoxidSensor accessory as CO sensor.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index ee02ea1a576..413786c22c4 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -413,14 +413,40 @@ def density_to_air_quality(density: float) -> int: def density_to_air_quality_pm10(density: float) -> int: - """Map PM10 density to HomeKit AirQuality level.""" - if density <= 40: + """Map PM10 µg/m3 density to HomeKit AirQuality level.""" + if density <= 54: # US AQI 0-50 (HomeKit: Excellent) return 1 - if density <= 80: + if density <= 154: # US AQI 51-100 (HomeKit: Good) return 2 - if density <= 120: + if density <= 254: # US AQI 101-150 (HomeKit: Fair) return 3 - if density <= 300: + if density <= 354: # US AQI 151-200 (HomeKit: Inferior) + return 4 + return 5 # US AQI 201+ (HomeKit: Poor) + + +def density_to_air_quality_nitrogen_dioxide(density: float) -> int: + """Map nitrogen dioxide µg/m3 to HomeKit AirQuality level.""" + if density <= 30: + return 1 + if density <= 60: + return 2 + if density <= 80: + return 3 + if density <= 90: + return 4 + return 5 + + +def density_to_air_quality_voc(density: float) -> int: + """Map VOCs µg/m3 to HomeKit AirQuality level.""" + if density <= 24: + return 1 + if density <= 48: + return 2 + if density <= 64: + return 3 + if density <= 96: return 4 return 5 diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 54da6e71c8c..aa56bbbda78 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice -from .const import KNOWN_DEVICES, TRIGGERS +from .const import KNOWN_DEVICES from .utils import async_get_controller _LOGGER = logging.getLogger(__name__) @@ -59,7 +59,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_get_controller(hass) hass.data[KNOWN_DEVICES] = {} - hass.data[TRIGGERS] = {} async def _async_stop_homekit_controller(event: Event) -> None: await asyncio.gather( diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index 4ce2b425a5e..11e935df455 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -84,7 +84,7 @@ async def async_setup_entry( entity.old_unique_id, entity.unique_id, Platform.BUTTON ) - async_add_entities(entities, True) + async_add_entities(entities) return True conn.add_char_factory(async_add_characteristic) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 41e88725121..2dc998200a4 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -148,9 +148,9 @@ class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity): ) @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" - features = 0 + features = ClimateEntityFeature(0) if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): features |= ClimateEntityFeature.FAN_MODE @@ -368,7 +368,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): ) @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = super().supported_features @@ -602,7 +602,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): return [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = super().supported_features diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index da4ccfe9f9a..1cfedb05847 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -420,6 +420,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Should never call this step without setting self.hkid assert self.hkid + description_placeholders = {} errors = {} @@ -465,10 +466,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="accessory_not_found_error") except InsecureSetupCode: errors["pairing_code"] = "insecure_setup_code" - except Exception: # pylint: disable=broad-except + except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Pairing attempt failed with an unhandled exception") self.finish_pairing = None errors["pairing_code"] = "pairing_failed" + description_placeholders["error"] = str(err) if not self.finish_pairing: # Its possible that the first try may have been busy so @@ -496,11 +498,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # TLV error, usually not in pairing mode _LOGGER.exception("Pairing communication failed") return await self.async_step_protocol_error() - except Exception: # pylint: disable=broad-except + except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Pairing attempt failed with an unhandled exception") errors["pairing_code"] = "pairing_failed" + description_placeholders["error"] = str(err) - return self._async_step_pair_show_form(errors) + return self._async_step_pair_show_form(errors, description_placeholders) async def async_step_busy_error( self, user_input: dict[str, Any] | None = None @@ -531,7 +534,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_step_pair_show_form( - self, errors: dict[str, str] | None = None + self, + errors: dict[str, str] | None = None, + description_placeholders: dict[str, str] | None = None, ) -> FlowResult: assert self.category @@ -547,7 +552,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="pair", errors=errors or {}, - description_placeholders=placeholders, + description_placeholders=placeholders | (description_placeholders or {}), data_schema=vol.Schema(schema), ) @@ -578,6 +583,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): accessories_state.config_num, accessories_state.accessories.serialize(), serialize_broadcast_key(accessories_state.broadcast_key), + accessories_state.state_num, ) return self.async_create_entry(title=name, data=pairing_data) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 320df671144..d230fe64517 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Callable -from datetime import timedelta +from collections.abc import Callable, Iterable +from datetime import datetime, timedelta import logging from types import MappingProxyType from typing import Any @@ -20,8 +20,9 @@ from aiohomekit.model.services import Service from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback +from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -29,6 +30,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CHARACTERISTIC_PLATFORMS, CONTROLLER, + DEBOUNCE_COOLDOWN, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, IDENTIFIER_ACCESSORY_ID, @@ -41,6 +43,8 @@ from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 + + BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds _LOGGER = logging.getLogger(__name__) @@ -110,17 +114,8 @@ class HKDevice: self.signal_state_updated = "_".join((DOMAIN, self.unique_id, "state_updated")) - # Current values of all characteristics homekit_controller is tracking. - # Key is a (accessory_id, characteristic_id) tuple. - self.current_state: dict[tuple[int, int], Any] = {} - self.pollable_characteristics: list[tuple[int, int]] = [] - # If this is set polling is active and can be disabled by calling - # this method. - self._polling_interval_remover: CALLBACK_TYPE | None = None - self._ble_available_interval_remover: CALLBACK_TYPE | None = None - # Never allow concurrent polling of the same accessory or bridge self._polling_lock = asyncio.Lock() self._polling_lock_warned = False @@ -131,6 +126,14 @@ class HKDevice: self.watchable_characteristics: list[tuple[int, int]] = [] + self._debounced_update = Debouncer( + hass, + _LOGGER, + cooldown=DEBOUNCE_COOLDOWN, + immediate=False, + function=self.async_update, + ) + @property def entity_map(self) -> Accessories: """Return the accessories from the pairing.""" @@ -177,8 +180,8 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) - async def _async_retry_populate_ble_accessory_state(self, event: Event) -> None: - """Try again to populate the BLE accessory state. + async def _async_populate_ble_accessory_state(self, event: Event) -> None: + """Populate the BLE accessory state without blocking startup. If the accessory was asleep at startup we need to retry since we continued on to allow startup to proceed. @@ -186,6 +189,7 @@ class HKDevice: If this fails the state may be inconsistent, but will get corrected as soon as the accessory advertises again. """ + self._async_start_polling() try: await self.pairing.async_populate_accessories_state(force_update=True) except STARTUP_EXCEPTIONS as ex: @@ -213,20 +217,28 @@ class HKDevice: # so we only poll those chars but that is not possible # yet. attempts = None if self.hass.state == CoreState.running else 1 - try: - await self.pairing.async_populate_accessories_state( - force_update=True, attempts=attempts - ) - except AccessoryNotFoundError: - if transport != Transport.BLE or not pairing.accessories: - # BLE devices may sleep and we can't force a connection - raise + if ( + transport == Transport.BLE + and pairing.accessories + and pairing.accessories.has_aid(1) + ): + # The GSN gets restored and a catch up poll will be + # triggered via disconnected events automatically + # if we are out of sync. To be sure we are in sync; + # If for some reason the BLE connection failed + # previously we force an update after startup + # is complete. entry.async_on_unload( self.hass.bus.async_listen( EVENT_HOMEASSISTANT_STARTED, - self._async_retry_populate_ble_accessory_state, + self._async_populate_ble_accessory_state, ) ) + else: + await self.pairing.async_populate_accessories_state( + force_update=True, attempts=attempts + ) + self._async_start_polling() entry.async_on_unload(pairing.dispatcher_connect(self.process_new_events)) entry.async_on_unload( @@ -244,24 +256,34 @@ class HKDevice: self.async_set_available_state(self.pairing.is_available) - self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, self.pairing.poll_interval - ) - if transport == Transport.BLE: # If we are using BLE, we need to periodically check of the # BLE device is available since we won't get callbacks # when it goes away since we HomeKit supports disconnected # notifications and we cannot treat a disconnect as unavailability. - self._ble_available_interval_remover = async_track_time_interval( - self.hass, - self.async_update_available_state, - timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL), + entry.async_on_unload( + async_track_time_interval( + self.hass, + self.async_update_available_state, + timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL), + ) ) # BLE devices always get an RSSI sensor as well if "sensor" not in self.platforms: await self.async_load_platform("sensor") + @callback + def _async_start_polling(self) -> None: + """Start polling for updates.""" + # We use async_request_update to avoid multiple updates + # at the same time which would generate a spurious warning + # in the log about concurrent polling. + self.config_entry.async_on_unload( + async_track_time_interval( + self.hass, self.async_request_update, self.pairing.poll_interval + ) + ) + async def async_add_new_entities(self) -> None: """Add new entities to Home Assistant.""" await self.async_load_platforms() @@ -518,9 +540,6 @@ class HKDevice: async def async_unload(self) -> None: """Stop interacting with device and prepare for removal from hass.""" - if self._polling_interval_remover: - self._polling_interval_remover() - await self.pairing.shutdown() await self.hass.config_entries.async_unload_platforms( @@ -635,6 +654,10 @@ class HKDevice: """Update the available state of the device.""" self.async_set_available_state(self.pairing.is_available) + async def async_request_update(self, now: datetime | None = None) -> None: + """Request an debounced update from the accessory.""" + await self._debounced_update.async_call() + async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" if not self.pollable_characteristics: @@ -685,50 +708,28 @@ class HKDevice: _LOGGER.debug("Finished HomeKit controller update: %s", self.unique_id) - def process_new_events(self, new_values_dict) -> None: + def process_new_events( + self, new_values_dict: dict[tuple[int, int], dict[str, Any]] + ) -> None: """Process events from accessory into HA state.""" self.async_set_available_state(True) # Process any stateless events (via device_triggers) async_fire_triggers(self, new_values_dict) - for (aid, cid), value in new_values_dict.items(): - accessory = self.current_state.setdefault(aid, {}) - accessory[cid] = value - - # self.current_state will be replaced by entity_map in a future PR - # For now we update both self.entity_map.process_changes(new_values_dict) async_dispatcher_send(self.hass, self.signal_state_updated) - async def get_characteristics(self, *args, **kwargs) -> dict[str, Any]: + async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Read latest state from homekit accessory.""" return await self.pairing.get_characteristics(*args, **kwargs) - async def put_characteristics(self, characteristics) -> None: + async def put_characteristics( + self, characteristics: Iterable[tuple[int, int, Any]] + ) -> None: """Control a HomeKit device state from Home Assistant.""" - results = await self.pairing.put_characteristics(characteristics) - - # Feed characteristics back into HA and update the current state - # results will only contain failures, so anythin in characteristics - # but not in results was applied successfully - we can just have HA - # reflect the change immediately. - - new_entity_state = {} - for aid, iid, value in characteristics: - key = (aid, iid) - - # If the key was returned by put_characteristics() then the - # change didn't work - if key in results: - continue - - # Otherwise it was accepted and we can apply the change to - # our state - new_entity_state[key] = {"value": value} - - self.process_new_events(new_entity_state) + await self.pairing.put_characteristics(characteristics) @property def unique_id(self) -> str: diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 8c7db4dad00..8b6a3e5fd30 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -107,3 +107,10 @@ STARTUP_EXCEPTIONS = ( EncryptionError, AccessoryDisconnectedError, ) + +# 10 seconds was chosen because it is soon enough +# for most state changes to happen but not too +# long that the BLE connection is dropped. It +# also happens to be the same value used by +# the update coordinator. +DEBOUNCE_COOLDOWN = 10 # seconds diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index d4feeccc77a..fbe6f08bc75 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -143,7 +143,7 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity): ] @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" features = ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index bc1434f4bd9..229c8aecc00 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -224,7 +224,7 @@ async def async_setup_triggers_for_entry( # They have to be different accessories (they can be on the same bridge) # In practice, this is inline with what iOS actually supports AFAWCT. device_id = conn.devices[aid] - if device_id in hass.data[TRIGGERS]: + if TRIGGERS in hass.data and device_id in hass.data[TRIGGERS]: return False # Just because we recognize the service type doesn't mean we can actually @@ -246,15 +246,18 @@ def async_get_or_create_trigger_source( hass: HomeAssistant, device_id: str ) -> TriggerSource: """Get or create a trigger source for a device id.""" - if not (source := hass.data[TRIGGERS].get(device_id)): + trigger_sources: dict[str, TriggerSource] = hass.data.setdefault(TRIGGERS, {}) + if not (source := trigger_sources.get(device_id)): source = TriggerSource(hass) - hass.data[TRIGGERS][device_id] = source + trigger_sources[device_id] = source return source def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], dict[str, Any]]): """Process events generated by a HomeKit accessory into automation triggers.""" - trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS] + trigger_sources: dict[str, TriggerSource] = conn.hass.data.get(TRIGGERS, {}) + if not trigger_sources: + return for (aid, iid), ev in events.items(): if aid in conn.devices: device_id = conn.devices[aid] diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index a4e1b2b41b3..eecf8b9c080 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -30,7 +30,6 @@ class HomeKitEntity(Entity): self._aid = devinfo["aid"] self._iid = devinfo["iid"] self._char_name: str | None = None - self._features = 0 self.setup() super().__init__() @@ -175,6 +174,10 @@ class HomeKitEntity(Entity): """Define the homekit characteristics the entity cares about.""" raise NotImplementedError + async def async_update(self) -> None: + """Update the entity.""" + await self._accessory.async_request_update() + class AccessoryEntity(HomeKitEntity): """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index cdd9c3e803c..550f86ddbe4 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -95,9 +95,9 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): return oscillating == 1 @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - features = 0 + features = FanEntityFeature(0) if self.service.has(CharacteristicsTypes.ROTATION_DIRECTION): features |= FanEntityFeature.DIRECTION diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index a6c8a3672a3..df03a1fef34 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -124,6 +124,10 @@ class HomeKitLock(HomeKitEntity, LockEntity): await self.async_put_characteristics( {CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: TARGET_STATE_MAP[state]} ) + # Some locks need to be polled to update the current state + # after a target state change. + # https://github.com/home-assistant/core/issues/81887 + await self._accessory.async_request_update() @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index f0438a7b841..47112d3bd50 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.19"], + "requirements": ["aiohomekit==2.4.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 4efa7dbce1c..1efa33429b1 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -80,9 +80,9 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): ] @property - def supported_features(self) -> int: + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - features = 0 + features = MediaPlayerEntityFeature(0) if self.service.has(CharacteristicsTypes.ACTIVE_IDENTIFIER): features |= MediaPlayerEntityFeature.SELECT_SOURCE diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index a20ba83e80a..5d72516bc06 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -78,7 +78,7 @@ async def async_setup_entry( entity.old_unique_id, entity.unique_id, Platform.NUMBER ) - async_add_entities(entities, True) + async_add_entities(entities) return True conn.add_char_factory(async_add_characteristic) diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index a5afb07620a..de4f23ad8da 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -61,11 +61,15 @@ class EntityMapStorage: config_num: int, accessories: list[Any], broadcast_key: str | None = None, + state_num: int | None = None, ) -> Pairing: """Create a new pairing cache.""" _LOGGER.debug("Creating or updating entity map for %s", homekit_id) data = Pairing( - config_num=config_num, accessories=accessories, broadcast_key=broadcast_key + config_num=config_num, + accessories=accessories, + broadcast_key=broadcast_key, + state_num=state_num, ) self.storage_data[homekit_id] = data self._async_schedule_save() diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 2831dabc38d..201e0a9b3c2 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -37,7 +37,7 @@ "unknown_error": "Device reported an unknown error. Pairing failed.", "authentication_error": "Incorrect HomeKit code. Please check it and try again.", "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", - "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently." + "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently: {error}" }, "abort": { "no_devices": "No unpaired devices could be found", diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 2b61efe6892..904a438d699 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -14,7 +14,7 @@ "authentication_error": "Ung\u00fcltiger HomeKit Code, \u00fcberpr\u00fcfe bitte den Code und versuche es erneut.", "insecure_setup_code": "Der angeforderte Setup-Code ist unsicher, da er zu trivial ist. Dieses Zubeh\u00f6r erf\u00fcllt nicht die grundlegenden Sicherheitsanforderungen.", "max_peers_error": "Das Ger\u00e4t weigerte sich, die Kopplung durchzuf\u00fchren, da es keinen freien Kopplungs-Speicher hat.", - "pairing_failed": "Beim Versuch dieses Ger\u00e4t zu koppeln ist ein Fehler aufgetreten. Dies kann ein vor\u00fcbergehender Fehler sein oder das Ger\u00e4t wird derzeit m\u00f6glicherweise nicht unterst\u00fctzt.", + "pairing_failed": "Beim Koppeln mit diesem Ger\u00e4t ist ein nicht behandelter Fehler aufgetreten. Dies kann ein vor\u00fcbergehender Fehler sein oder das oder dein Ger\u00e4t wird derzeit nicht unterst\u00fctzt: {error}", "unable_to_pair": "Koppeln fehltgeschlagen, bitte versuche es erneut", "unknown_error": "Das Ger\u00e4t meldete einen unbekannten Fehler. Die Kopplung ist fehlgeschlagen." }, diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 2686e71d252..c5b5178a58b 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -14,7 +14,7 @@ "authentication_error": "Incorrect HomeKit code. Please check it and try again.", "insecure_setup_code": "The requested setup code is insecure because of its trivial nature. This accessory fails to meet basic security requirements.", "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", - "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.", + "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently: {error}", "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed." }, diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index a43373cd399..8e3eb44475b 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -14,7 +14,7 @@ "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo se neg\u00f3 a a\u00f1adir el emparejamiento porque no tiene almacenamiento disponible para emparejamientos.", - "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible por el momento.", + "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o es posible que tu dispositivo no sea compatible actualmente: {error}", "unable_to_pair": "No se puede emparejar, por favor, int\u00e9ntalo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. El emparejamiento ha fallado." }, diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json index 8db1df29803..1cefd89db57 100644 --- a/homeassistant/components/homekit_controller/translations/et.json +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -14,7 +14,7 @@ "authentication_error": "Vale HomeKiti kood. Kontrolli seda ja proovi uuesti.", "insecure_setup_code": "Taotletud salas\u00f5na on ebaturvaline, sest see on liiga lihtne ning ei vasta p\u00f5hilistele turvan\u00f5uetele.", "max_peers_error": "Seade keeldus sidumist lisamast kuna puudub piisav salvestusruum.", - "pairing_failed": "Selle seadmega sidumise katsel ilmnes tundmatu t\u00f5rge. See v\u00f5ib olla ajutine t\u00f5rge v\u00f5i seadet ei toetata praegu.", + "pairing_failed": "Selle seadmega sidumise katsel ilmnes ootamatu t\u00f5rge. See v\u00f5ib olla ajutine rike v\u00f5i seadet ei toetata praegu: {error}", "unable_to_pair": "Ei saa siduda, proovi uuesti.", "unknown_error": "Seade teatas tundmatust t\u00f5rkest. Sidumine nurjus." }, diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json index 0514fcca4ed..aa2d5111756 100644 --- a/homeassistant/components/homekit_controller/translations/id.json +++ b/homeassistant/components/homekit_controller/translations/id.json @@ -14,7 +14,7 @@ "authentication_error": "Kode HomeKit salah. Periksa dan coba lagi.", "insecure_setup_code": "Kode penyiapan yang diminta tidak aman karena sifatnya yang sepele. Aksesori ini gagal memenuhi persyaratan keamanan dasar.", "max_peers_error": "Perangkat menolak untuk menambahkan pemasangan karena tidak memiliki penyimpanan pemasangan yang tersedia.", - "pairing_failed": "Terjadi kesalahan yang tidak tertangani saat mencoba memasangkan dengan perangkat ini. Ini mungkin kegagalan sementara atau perangkat Anda mungkin tidak didukung saat ini.", + "pairing_failed": "Terjadi kesalahan yang tidak tertangani saat mencoba memasangkan dengan perangkat ini. Ini mungkin kegagalan sementara atau perangkat Anda mungkin tidak didukung saat ini: {error}", "unable_to_pair": "Gagal memasangkan, coba lagi.", "unknown_error": "Perangkat melaporkan kesalahan yang tidak diketahui. Pemasangan gagal." }, diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index 70b74d51f64..3775734f7c0 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -14,7 +14,7 @@ "authentication_error": "Ugyldig HomeKit kode. Vennligst sjekk den og pr\u00f8v igjen.", "insecure_setup_code": "Den forespurte installasjonskoden er usikker p\u00e5 grunn av triviell natur. Dette tilbeh\u00f8ret oppfyller ikke grunnleggende sikkerhetskrav.", "max_peers_error": "Enheten nekter \u00e5 sammenkoble da den ikke har ledig sammenkoblingslagring.", - "pairing_failed": "En uh\u00e5ndtert feil oppstod under fors\u00f8k p\u00e5 \u00e5 koble til denne enheten. Dette kan v\u00e6re en midlertidig feil, eller at enheten din kan ikke st\u00f8ttes for \u00f8yeblikket.", + "pairing_failed": "Det oppstod en uh\u00e5ndtert feil under fors\u00f8k p\u00e5 \u00e5 pare med denne enheten. Dette kan v\u00e6re en midlertidig feil eller enheten din st\u00f8ttes kanskje ikke for \u00f8yeblikket: {error}", "unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.", "unknown_error": "Enheten rapporterte en ukjent feil. Sammenkobling mislyktes." }, diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index d7aba3a17c7..efd4f616553 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -14,7 +14,7 @@ "authentication_error": "Niepoprawny kod parowania HomeKit. Sprawd\u017a go i spr\u00f3buj ponownie.", "insecure_setup_code": "\u017b\u0105dany kod instalacyjny jest niezabezpieczony ze wzgl\u0119du na jego trywialny charakter. To akcesorium nie spe\u0142nia podstawowych wymaga\u0144 bezpiecze\u0144stwa.", "max_peers_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c nie ma wolnej pami\u0119ci parowania", - "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.", + "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane: {error}", "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, diff --git a/homeassistant/components/homekit_controller/translations/pt-BR.json b/homeassistant/components/homekit_controller/translations/pt-BR.json index f84ff6bac37..1055cd6c921 100644 --- a/homeassistant/components/homekit_controller/translations/pt-BR.json +++ b/homeassistant/components/homekit_controller/translations/pt-BR.json @@ -14,7 +14,7 @@ "authentication_error": "C\u00f3digo HomeKit incorreto. Por favor verifique e tente novamente.", "insecure_setup_code": "O c\u00f3digo de configura\u00e7\u00e3o solicitado \u00e9 inseguro devido \u00e0 sua natureza trivial. Este acess\u00f3rio n\u00e3o atende aos requisitos b\u00e1sicos de seguran\u00e7a.", "max_peers_error": "O dispositivo recusou-se a adicionar o emparelhamento, pois n\u00e3o tem armazenamento de emparelhamento gratuito.", - "pairing_failed": "Ocorreu um erro sem tratamento ao tentar emparelhar com este dispositivo. Isso pode ser uma falha tempor\u00e1ria ou o dispositivo pode n\u00e3o ser suportado no momento.", + "pairing_failed": "Ocorreu um erro n\u00e3o tratado ao tentar emparelhar com este dispositivo. Esta pode ser uma falha tempor\u00e1ria ou seu dispositivo pode n\u00e3o ser suportado atualmente: {error}", "unable_to_pair": "N\u00e3o \u00e9 poss\u00edvel parear, tente novamente.", "unknown_error": "O dispositivo relatou um erro desconhecido. O pareamento falhou." }, diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index 00a4d9fcb7c..0e7c4f94628 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -14,7 +14,7 @@ "authentication_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 HomeKit. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u0434 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "insecure_setup_code": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d \u0438\u0437-\u0437\u0430 \u0441\u0432\u043e\u0435\u0439 \u0442\u0440\u0438\u0432\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438. \u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u043d\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u043c \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.", "max_peers_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b\u043e \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0438\u0437-\u0437\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u0430.", - "pairing_failed": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0441\u0431\u043e\u0439 \u0438\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0435\u0449\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "pairing_failed": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0441\u0431\u043e\u0439 \u0438\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0435\u0449\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f: {error}", "unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u043e\u0431\u0449\u0438\u043b\u043e \u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435. \u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c." }, diff --git a/homeassistant/components/homekit_controller/translations/select.sk.json b/homeassistant/components/homekit_controller/translations/select.sk.json new file mode 100644 index 00000000000..62277db1800 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.sk.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Pre\u010d", + "home": "Doma", + "sleep": "Sp\u00e1nok" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.de.json b/homeassistant/components/homekit_controller/translations/sensor.de.json index 2aef45f5303..27a6d2a5e0b 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.de.json +++ b/homeassistant/components/homekit_controller/translations/sensor.de.json @@ -9,7 +9,7 @@ "sleepy": "Sleepy Endger\u00e4t" }, "homekit_controller__thread_status": { - "border_router": "Border-Router", + "border_router": "Border Router", "child": "Kind", "detached": "Freistehend", "disabled": "Deaktiviert", diff --git a/homeassistant/components/homekit_controller/translations/sensor.sk.json b/homeassistant/components/homekit_controller/translations/sensor.sk.json new file mode 100644 index 00000000000..9d22481d838 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.sk.json @@ -0,0 +1,8 @@ +{ + "state": { + "homekit_controller__thread_status": { + "disabled": "Zak\u00e1zan\u00e9", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sk.json b/homeassistant/components/homekit_controller/translations/sk.json index 8ebd0e1e08e..a62d9c6334a 100644 --- a/homeassistant/components/homekit_controller/translations/sk.json +++ b/homeassistant/components/homekit_controller/translations/sk.json @@ -1,12 +1,61 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "accessory_not_found_error": "Nie je mo\u017en\u00e9 prida\u0165 p\u00e1rovanie, preto\u017ee zariadenie u\u017e nie je mo\u017en\u00e9 n\u00e1js\u0165.", + "already_configured": "Pr\u00edslu\u0161enstvo je u\u017e nakonfigurovan\u00e9 s t\u00fdmto kontrol\u00e9rom.", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "already_paired": "Toto pr\u00edslu\u0161enstvo je u\u017e sp\u00e1rovan\u00e9 s in\u00fdm zariaden\u00edm. Resetujte pr\u00edslu\u0161enstvo a sk\u00faste to znova.", + "no_devices": "Nena\u0161li sa \u017eiadne nesp\u00e1rovan\u00e9 zariadenia" }, + "error": { + "authentication_error": "Nespr\u00e1vny k\u00f3d HomeKit. Skontrolujte to a sk\u00faste to znova.", + "pairing_failed": "Pri pokuse o sp\u00e1rovanie s t\u00fdmto zariaden\u00edm do\u0161lo k neobsluhovanej chybe. M\u00f4\u017ee \u00eds\u0165 o do\u010dasn\u00fa chybu alebo va\u0161e zariadenie nemus\u00ed by\u0165 v s\u00fa\u010dasnosti podporovan\u00e9: {error}", + "unable_to_pair": "Nie je mo\u017en\u00e9 sp\u00e1rova\u0165, sk\u00faste to znova.", + "unknown_error": "Zariadenie hl\u00e1silo nezn\u00e1mu chybu. Sp\u00e1rovanie zlyhalo." + }, + "flow_title": "{name} ({category})", "step": { + "busy_error": { + "title": "Zariadenie sa u\u017e sp\u00e1ruje s in\u00fdm kontrol\u00e9rom" + }, + "max_tries_error": { + "title": "Bol prekro\u010den\u00fd maxim\u00e1lny po\u010det pokusov o overenie" + }, + "pair": { + "data": { + "allow_insecure_setup_codes": "Povoli\u0165 p\u00e1rovanie s nezabezpe\u010den\u00fdmi k\u00f3dmi nastavenia.", + "pairing_code": "K\u00f3d p\u00e1rovania" + } + }, + "protocol_error": { + "title": "Chyba pri komunik\u00e1cii s pr\u00edslu\u0161enstvom" + }, "user": { + "data": { + "device": "Zariadenie" + }, "title": "V\u00fdber zariadenia" } } + }, + "device_automation": { + "trigger_subtype": { + "button1": "Tla\u010didlo 1", + "button10": "Tla\u010didlo 10", + "button2": "Tla\u010didlo 2", + "button3": "Tla\u010didlo 3", + "button4": "Tla\u010didlo 4", + "button5": "Tla\u010didlo 5", + "button6": "Tla\u010didlo 6", + "button7": "Tla\u010didlo 7", + "button8": "Tla\u010didlo 8", + "button9": "Tla\u010didlo 9", + "doorbell": "Domov\u00fd zvon\u010dek" + }, + "trigger_type": { + "double_press": "\"{subtype}\" stla\u010den\u00e9 dvakr\u00e1t", + "long_press": "\"{subtype}\" stla\u010den\u00e9 a podr\u017ean\u00e9", + "single_press": "\"{subtype}\" stla\u010den\u00e9" + } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index 63b4b45f520..4614676e797 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -14,7 +14,7 @@ "authentication_error": "Homekit \u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u5b9a\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "insecure_setup_code": "\u7531\u65bc\u5176\u7463\u788e\u7279\u6027\u3001\u6240\u8acb\u6c42\u7684\u8a2d\u5b9a\u4ee3\u78bc\u4e0d\u5b89\u5168\u3002\u6b64\u914d\u4ef6\u7121\u6cd5\u9054\u5230\u6700\u4f4e\u5b89\u5168\u9700\u6c42\u3002", "max_peers_error": "\u88dd\u7f6e\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "pairing_failed": "\u7576\u8a66\u5716\u8207\u88dd\u7f6e\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u88dd\u7f6e\u76ee\u524d\u4e0d\u652f\u63f4\u3002", + "pairing_failed": "\u7576\u8a66\u5716\u8207\u88dd\u7f6e\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u88dd\u7f6e\u76ee\u524d\u4e0d\u652f\u63f4\uff1a{error}", "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 38a75266a17..87f3dfb314a 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -82,9 +82,9 @@ class HMLight(HMDevice, LightEntity): return color_modes @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag supported features.""" - features: int = LightEntityFeature.TRANSITION + features = LightEntityFeature.TRANSITION if "PROGRAM" in self._hmdevice.WRITENODE: features |= LightEntityFeature.EFFECT return features diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index c7a78c7bbcf..4c18afd0458 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( DEGREE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_MILLIMETERS, @@ -141,7 +142,7 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { ), "IEC_ENERGY_COUNTER": SensorEntityDescription( key="IEC_ENERGY_COUNTER", - native_unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -252,6 +253,36 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), + "STATE": SensorEntityDescription( + key="STATE", + ), + "SMOKE_DETECTOR_ALARM_STATUS": SensorEntityDescription( + key="SMOKE_DETECTOR_ALARM_STATUS", + ), + "WIND_DIR": SensorEntityDescription( + key="WIND_DIR", + ), + "WIND_DIR_RANGE": SensorEntityDescription( + key="WIND_DIR_RANGE", + ), + "CONCENTRATION_STATUS": SensorEntityDescription( + key="CONCENTRATION_STATUS", + ), + "PASSAGE_COUNTER_VALUE": SensorEntityDescription( + key="PASSAGE_COUNTER_VALUE", + ), + "LEVEL": SensorEntityDescription( + key="LEVEL", + ), + "LEVEL_2": SensorEntityDescription( + key="LEVEL_2", + ), + "DOOR_STATE": SensorEntityDescription( + key="DOOR_STATE", + ), + "FILLING_LEVEL": SensorEntityDescription( + key="FILLING_LEVEL", + ), } DEFAULT_SENSOR_DESCRIPTION = SensorEntityDescription( diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index db35a5d3ee5..fb4bfdd637e 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -195,7 +195,7 @@ class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP base action sensor.""" @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.MOVING @@ -240,7 +240,7 @@ class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEnt ) @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.OPENING @@ -274,7 +274,7 @@ class HomematicipShutterContact(HomematicipMultiContactInterface, BinarySensorEn self.has_additional_state = has_additional_state @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.DOOR @@ -295,7 +295,7 @@ class HomematicipMotionDetector(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP motion detector.""" @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.MOTION @@ -309,7 +309,7 @@ class HomematicipPresenceDetector(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP presence detector.""" @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.PRESENCE @@ -323,7 +323,7 @@ class HomematicipSmokeDetector(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP smoke detector.""" @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.SMOKE @@ -342,7 +342,7 @@ class HomematicipWaterDetector(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP water detector.""" @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.MOISTURE @@ -378,7 +378,7 @@ class HomematicipRainSensor(HomematicipGenericEntity, BinarySensorEntity): super().__init__(hap, device, "Raining") @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.MOISTURE @@ -396,7 +396,7 @@ class HomematicipSunshineSensor(HomematicipGenericEntity, BinarySensorEntity): super().__init__(hap, device, post="Sunshine") @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.LIGHT @@ -425,7 +425,7 @@ class HomematicipBatterySensor(HomematicipGenericEntity, BinarySensorEntity): super().__init__(hap, device, post="Battery") @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.BATTERY @@ -445,7 +445,7 @@ class HomematicipPluggableMainsFailureSurveillanceSensor( super().__init__(hap, device) @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.POWER @@ -464,7 +464,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorE super().__init__(hap, device, post=post) @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" return BinarySensorDeviceClass.SAFETY diff --git a/homeassistant/components/homematicip_cloud/button.py b/homeassistant/components/homematicip_cloud/button.py new file mode 100644 index 00000000000..3fb8ebe20bd --- /dev/null +++ b/homeassistant/components/homematicip_cloud/button.py @@ -0,0 +1,41 @@ +"""Support for HomematicIP Cloud button devices.""" +from __future__ import annotations + +from homematicip.aio.device import AsyncWallMountedGarageDoorController + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity +from .hap import HomematicipHAP + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the HomematicIP button from a config entry.""" + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] + entities: list[HomematicipGenericEntity] = [] + for device in hap.home.devices: + if isinstance(device, AsyncWallMountedGarageDoorController): + entities.append(HomematicipGarageDoorControllerButton(hap, device)) + + if entities: + async_add_entities(entities) + + +class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEntity): + """Representation of the HomematicIP Wall mounted Garage Door Controller.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize a wall mounted garage door controller.""" + super().__init__(hap, device) + self._attr_icon = "mdi:arrow-up-down" + + async def async_press(self) -> None: + """Handle the button press.""" + await self._device.send_start_impulse() diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 2fc9f8fd12d..b6ac23b5c71 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Any -from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact +from homematicip.aio.device import ( + AsyncHeatingThermostat, + AsyncHeatingThermostatCompact, + AsyncHeatingThermostatEvo, +) from homematicip.aio.group import AsyncHeatingGroup from homematicip.base.enums import AbsenceType from homematicip.device import Switch @@ -312,11 +316,16 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): @property def _first_radiator_thermostat( self, - ) -> AsyncHeatingThermostat | AsyncHeatingThermostatCompact | None: + ) -> AsyncHeatingThermostat | AsyncHeatingThermostatCompact | AsyncHeatingThermostatEvo | None: """Return the first radiator thermostat from the hmip heating group.""" for device in self._device.devices: if isinstance( - device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact) + device, + ( + AsyncHeatingThermostat, + AsyncHeatingThermostatCompact, + AsyncHeatingThermostatEvo, + ), ): return device diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index a0f1c84015f..055db90a68c 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -10,6 +10,7 @@ DOMAIN = "homematicip_cloud" PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.LIGHT, diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 0d06d595f1b..d8d0b3e9836 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==1.0.7"], + "requirements": ["homematicip==1.0.11"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 03aaa7626b7..fdf125dbfec 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -8,6 +8,7 @@ from homematicip.aio.device import ( AsyncFullFlushSwitchMeasuring, AsyncHeatingThermostat, AsyncHeatingThermostatCompact, + AsyncHeatingThermostatEvo, AsyncHomeControlAccessPoint, AsyncLightSensor, AsyncMotionDetectorIndoor, @@ -75,7 +76,14 @@ async def async_setup_entry( for device in hap.home.devices: if isinstance(device, AsyncHomeControlAccessPoint): entities.append(HomematicipAccesspointDutyCycle(hap, device)) - if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): + if isinstance( + device, + ( + AsyncHeatingThermostat, + AsyncHeatingThermostatCompact, + AsyncHeatingThermostatEvo, + ), + ): entities.append(HomematicipHeatingThermostat(hap, device)) entities.append(HomematicipTemperatureSensor(hap, device)) if isinstance( @@ -197,7 +205,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Humidity") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.HUMIDITY @@ -222,7 +230,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Temperature") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.TEMPERATURE @@ -261,7 +269,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Illuminance") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.ILLUMINANCE @@ -300,7 +308,7 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Power") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.POWER @@ -325,7 +333,7 @@ class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): super().__init__(hap, device, post="Energy") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.ENERGY @@ -403,7 +411,7 @@ class HomematicpTemperatureExternalSensorCh1(HomematicipGenericEntity, SensorEnt super().__init__(hap, device, post="Channel 1 Temperature") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.TEMPERATURE @@ -428,7 +436,7 @@ class HomematicpTemperatureExternalSensorCh2(HomematicipGenericEntity, SensorEnt super().__init__(hap, device, post="Channel 2 Temperature") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.TEMPERATURE @@ -453,7 +461,7 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE super().__init__(hap, device, post="Delta Temperature") @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class of the sensor.""" return SensorDeviceClass.TEMPERATURE diff --git a/homeassistant/components/homematicip_cloud/translations/hr.json b/homeassistant/components/homematicip_cloud/translations/hr.json index 648dbfe73f9..a1e99ac9642 100644 --- a/homeassistant/components/homematicip_cloud/translations/hr.json +++ b/homeassistant/components/homematicip_cloud/translations/hr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "connection_aborted": "Povezivanje nije uspjelo", "unknown": "Do\u0161lo je do nepoznate pogre\u0161ke." } } diff --git a/homeassistant/components/homematicip_cloud/translations/sk.json b/homeassistant/components/homematicip_cloud/translations/sk.json index 48638e08787..209e87329e3 100644 --- a/homeassistant/components/homematicip_cloud/translations/sk.json +++ b/homeassistant/components/homematicip_cloud/translations/sk.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "connection_aborted": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "press_the_button": "Stla\u010dte pros\u00edm modr\u00e9 tla\u010didlo.", + "register_failed": "Registr\u00e1cia zlyhala, sk\u00faste to znova." + }, "step": { "init": { "data": { - "name": "N\u00e1zov (volite\u013en\u00e9, pou\u017e\u00edva sa ako predpona pre v\u0161etky zariadenia)" + "hapid": "ID pr\u00edstupov\u00e9ho bodu (SGTIN)", + "name": "N\u00e1zov (volite\u013en\u00e9, pou\u017e\u00edva sa ako predpona pre v\u0161etky zariadenia)", + "pin": "PIN k\u00f3d" } } } diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index a52e99bfa4e..e913e1125f1 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -22,7 +22,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,8 +70,8 @@ async def async_setup_entry( class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP weather sensor plus & basic.""" - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" @@ -126,8 +126,8 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP home weather.""" - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index ec43cdfdd2e..a236c392c07 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -5,7 +5,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN, PLATFORMS from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator @@ -71,6 +71,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.api.close() raise + # Register device + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + name=entry.title, + manufacturer="HomeWizard", + sw_version=coordinator.data["device"].firmware_version, + model=coordinator.data["device"].product_type, + identifiers={(DOMAIN, coordinator.data["device"].serial)}, + ) + # Finalize hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator diff --git a/homeassistant/components/homewizard/button.py b/homeassistant/components/homewizard/button.py new file mode 100644 index 00000000000..4bcc5016dec --- /dev/null +++ b/homeassistant/components/homewizard/button.py @@ -0,0 +1,57 @@ +"""Support for HomeWizard buttons.""" + +import logging + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import HWEnergyDeviceUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Identify button.""" + coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + features = await coordinator.api.features() + if features.has_identify: + async_add_entities([HomeWizardIdentifyButton(coordinator, entry)]) + + +class HomeWizardIdentifyButton( + CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], ButtonEntity +): + """Representation of a identify button.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: HWEnergyDeviceUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize button.""" + super().__init__(coordinator) + self._attr_unique_id = f"{entry.unique_id}_identify" + self._attr_device_info = { + "name": entry.title, + "manufacturer": "HomeWizard", + "sw_version": coordinator.data["device"].firmware_version, + "model": coordinator.data["device"].product_type, + "identifiers": {(DOMAIN, coordinator.data["device"].serial)}, + } + self._attr_name = "Identify" + self._attr_icon = "mdi:magnify" + self._attr_entity_category = EntityCategory.DIAGNOSTIC + + async def async_press(self) -> None: + """Identify the device.""" + await self.coordinator.api.identify() diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index c1c788d371b..e5ceb5ab23d 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -5,12 +5,12 @@ from datetime import timedelta from typing import TypedDict # Set up. -from homewizard_energy.models import Data, Device, State +from homewizard_energy.models import Data, Device, State, System from homeassistant.const import Platform DOMAIN = "homewizard" -PLATFORMS = [Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] # Platform config. CONF_API_ENABLED = "api_enabled" @@ -30,3 +30,4 @@ class DeviceResponseEntry(TypedDict): device: Device data: Data state: State + system: System diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index bab7b5d3ba3..dc441836d9a 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -8,6 +8,7 @@ from homewizard_energy.errors import DisabledError, RequestError from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry @@ -30,6 +31,13 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) self.api = HomeWizardEnergy(host, clientsession=async_get_clientsession(hass)) + @property + def device_info(self) -> DeviceInfo: + """Return device_info.""" + return DeviceInfo( + identifiers={(DOMAIN, self.data["device"].serial)}, + ) + async def _async_update_data(self) -> DeviceResponseEntry: """Fetch all device and sensor data from api.""" @@ -39,8 +47,13 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] "device": await self.api.device(), "data": await self.api.data(), "state": await self.api.state(), + "system": None, } + features = await self.api.features() + if features.has_system: + data["system"] = await self.api.system() + except RequestError as ex: raise UpdateFailed("Device did not respond as expected") from ex diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index a97d2507098..a0c852cf4b6 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -27,6 +27,9 @@ async def async_get_config_entry_diagnostics( "state": asdict(coordinator.data["state"]) if coordinator.data["state"] is not None else None, + "system": asdict(coordinator.data["system"]) + if coordinator.data["system"] is not None + else None, } return { diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 97b3c80b50d..baec844cc26 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": ["python-homewizard-energy==1.1.0"], + "requirements": ["python-homewizard-energy==1.3.1"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/homewizard/number.py b/homeassistant/components/homewizard/number.py new file mode 100644 index 00000000000..783841168ed --- /dev/null +++ b/homeassistant/components/homewizard/number.py @@ -0,0 +1,66 @@ +"""Creates HomeWizard Number entities.""" +from __future__ import annotations + +from typing import Optional, cast + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import HWEnergyDeviceUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up numbers for device.""" + coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if coordinator.data["state"]: + async_add_entities( + [ + HWEnergyNumberEntity(coordinator, entry), + ] + ) + + +class HWEnergyNumberEntity( + CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], NumberEntity +): + """Representation of status light number.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_has_entity_name = True + + def __init__( + self, + coordinator: HWEnergyDeviceUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the control number.""" + super().__init__(coordinator) + self._attr_unique_id = f"{entry.unique_id}_status_light_brightness" + self._attr_name = "Status light brightness" + self._attr_native_unit_of_measurement = PERCENTAGE + self._attr_icon = "mdi:lightbulb-on" + self._attr_device_info = coordinator.device_info + + async def async_set_native_value(self, value: float) -> None: + """Set a new value.""" + await self.coordinator.api.state_set(brightness=value * (255 / 100)) + await self.coordinator.async_refresh() + + @property + def native_value(self) -> float | None: + """Return the current value.""" + brightness = cast(Optional[float], self.coordinator.data["state"].brightness) + if brightness is None: + return None + return round(brightness * (100 / 255)) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 4baaff8835d..493b5f1c0e3 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -171,6 +171,7 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE # Config attributes. self.data_type = description.key self._attr_unique_id = f"{entry.unique_id}_{description.key}" + self._attr_device_info = coordinator.device_info # Special case for export, not everyone has solarpanels # The chance that 'export' is non-zero when you have solar panels is nil @@ -181,17 +182,6 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE if self.native_value == 0: self._attr_entity_registry_enabled_default = False - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return { - "name": self.entry.title, - "manufacturer": "HomeWizard", - "sw_version": self.data["device"].firmware_version, - "model": self.data["device"].product_type, - "identifiers": {(DOMAIN, self.data["device"].serial)}, - } - @property def data(self) -> DeviceResponseEntry: """Return data object from DataUpdateCoordinator.""" diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index eca8a7670be..3b255d195b1 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -22,13 +22,16 @@ async def async_setup_entry( """Set up switches.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[SwitchEntity] = [] + if coordinator.data["state"]: - async_add_entities( - [ - HWEnergyMainSwitchEntity(coordinator, entry), - HWEnergySwitchLockEntity(coordinator, entry), - ] - ) + entities.append(HWEnergyMainSwitchEntity(coordinator, entry)) + entities.append(HWEnergySwitchLockEntity(coordinator, entry)) + + if coordinator.data["system"]: + entities.append(HWEnergyEnableCloudEntity(hass, coordinator, entry)) + + async_add_entities(entities) class HWEnergySwitchEntity( @@ -47,13 +50,7 @@ class HWEnergySwitchEntity( """Initialize the switch.""" super().__init__(coordinator) self._attr_unique_id = f"{entry.unique_id}_{key}" - self._attr_device_info = { - "name": entry.title, - "manufacturer": "HomeWizard", - "sw_version": coordinator.data["device"].firmware_version, - "model": coordinator.data["device"].product_type, - "identifiers": {(DOMAIN, coordinator.data["device"].serial)}, - } + self._attr_device_info = coordinator.device_info class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): @@ -124,3 +121,47 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): def is_on(self) -> bool: """Return true if switch is on.""" return bool(self.coordinator.data["state"].switch_lock) + + +class HWEnergyEnableCloudEntity(HWEnergySwitchEntity): + """ + Representation of the enable cloud configuration. + + Turning off 'cloud connection' turns off all communication to HomeWizard Cloud. + At this point, the device is fully local. + """ + + _attr_name = "Cloud connection" + _attr_device_class = SwitchDeviceClass.SWITCH + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + hass: HomeAssistant, + coordinator: HWEnergyDeviceUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator, entry, "cloud_connection") + self.hass = hass + self.entry = entry + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn cloud connection on.""" + await self.coordinator.api.system_set(cloud_enabled=True) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn cloud connection off.""" + await self.coordinator.api.system_set(cloud_enabled=False) + await self.coordinator.async_refresh() + + @property + def icon(self) -> str | None: + """Return the icon.""" + return "mdi:cloud" if self.is_on else "mdi:cloud-off-outline" + + @property + def is_on(self) -> bool: + """Return true if cloud connection is active.""" + return bool(self.coordinator.data["system"].cloud_enabled) diff --git a/homeassistant/components/homewizard/translations/de.json b/homeassistant/components/homewizard/translations/de.json index 782ac2bf6fe..f9dc268f99b 100644 --- a/homeassistant/components/homewizard/translations/de.json +++ b/homeassistant/components/homewizard/translations/de.json @@ -16,7 +16,7 @@ "data": { "ip_address": "IP-Adresse" }, - "description": "Gib die IP-Adresse deines HomeWizard Energy-Ger\u00e4ts ein, um es in Home Assistant zu integrieren.", + "description": "Gib die IP-Adresse deines HomeWizard Energy Ger\u00e4ts ein, um es in Home Assistant zu integrieren.", "title": "Ger\u00e4t konfigurieren" } } diff --git a/homeassistant/components/homewizard/translations/sk.json b/homeassistant/components/homewizard/translations/sk.json new file mode 100644 index 00000000000..2dc757a5250 --- /dev/null +++ b/homeassistant/components/homewizard/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "device_not_supported": "Toto zariadenie nie je podporovan\u00e9", + "invalid_discovery_parameters": "Zisten\u00e1 nepodporovan\u00e1 verzia API", + "unknown_error": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {product_type} ({serial}) na {ip_address}?", + "title": "Potvrdi\u0165" + }, + "user": { + "data": { + "ip_address": "IP adresa" + }, + "title": "Konfigur\u00e1cia zariadenia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/sk.json b/homeassistant/components/honeywell/translations/sk.json index 1b1e671c054..04cb927ad6f 100644 --- a/homeassistant/components/honeywell/translations/sk.json +++ b/homeassistant/components/honeywell/translations/sk.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "password": "Heslo" + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index c75d47d06eb..680d2982f9f 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -86,7 +86,6 @@ class HorizonDevice(MediaPlayerEntity): """Initialize the remote.""" self._client = client self._name = name - self._state = None self._keys = remote_keys @property @@ -94,66 +93,61 @@ class HorizonDevice(MediaPlayerEntity): """Return the name of the remote.""" return self._name - @property - def state(self): - """Return the state of the device.""" - return self._state - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self) -> None: """Update State using the media server running on the Horizon.""" try: if self._client.is_powered_on(): - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING else: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF except OSError: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF def turn_on(self) -> None: """Turn the device on.""" - if self._state == MediaPlayerState.OFF: + if self.state == MediaPlayerState.OFF: self._send_key(self._keys.POWER) def turn_off(self) -> None: """Turn the device off.""" - if self._state != MediaPlayerState.OFF: + if self.state != MediaPlayerState.OFF: self._send_key(self._keys.POWER) def media_previous_track(self) -> None: """Channel down.""" self._send_key(self._keys.CHAN_DOWN) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def media_next_track(self) -> None: """Channel up.""" self._send_key(self._keys.CHAN_UP) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def media_play(self) -> None: """Send play command.""" self._send_key(self._keys.PAUSE) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def media_pause(self) -> None: """Send pause command.""" self._send_key(self._keys.PAUSE) - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED def media_play_pause(self) -> None: """Send play/pause command.""" self._send_key(self._keys.PAUSE) - if self._state == MediaPlayerState.PAUSED: - self._state = MediaPlayerState.PLAYING + if self.state == MediaPlayerState.PAUSED: + self._attr_state = MediaPlayerState.PLAYING else: - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media / switch to channel.""" if MediaType.CHANNEL == media_type: try: self._select_channel(int(media_id)) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING except ValueError: _LOGGER.error("Invalid channel: %s", media_id) else: diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index a6ca769250f..30465c9bd81 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -99,6 +99,7 @@ SCHEMA_WS_APPKEY = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( # The number of days after the moment a notification is sent that a JWT # is valid. JWT_VALID_DAYS = 7 +VAPID_CLAIM_VALID_HOURS = 12 KEYS_SCHEMA = vol.All( dict, @@ -514,7 +515,10 @@ class HTML5NotificationService(BaseNotificationService): webpusher = WebPusher(info[ATTR_SUBSCRIPTION]) if self._vapid_prv and self._vapid_email: vapid_headers = create_vapid_headers( - self._vapid_email, info[ATTR_SUBSCRIPTION], self._vapid_prv + self._vapid_email, + info[ATTR_SUBSCRIPTION], + self._vapid_prv, + timestamp, ) vapid_headers.update({"urgency": priority, "priority": priority}) response = webpusher.send( @@ -540,6 +544,12 @@ class HTML5NotificationService(BaseNotificationService): _LOGGER.error("Error saving registration") else: _LOGGER.info("Configuration saved") + elif response.status_code > 399: + _LOGGER.error( + "There was an issue sending the notification %s: %s", + response.status, + response.text, + ) def add_jwt(timestamp, target, tag, jwt_secret): @@ -556,14 +566,23 @@ def add_jwt(timestamp, target, tag, jwt_secret): return jwt.encode(jwt_claims, jwt_secret) -def create_vapid_headers(vapid_email, subscription_info, vapid_private_key): +def create_vapid_headers(vapid_email, subscription_info, vapid_private_key, timestamp): """Create encrypted headers to send to WebPusher.""" - if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info: + if ( + vapid_email + and vapid_private_key + and ATTR_ENDPOINT in subscription_info + and timestamp + ): + vapid_exp = datetime.fromtimestamp(timestamp) + timedelta( + hours=VAPID_CLAIM_VALID_HOURS + ) url = urlparse(subscription_info.get(ATTR_ENDPOINT)) vapid_claims = { "sub": f"mailto:{vapid_email}", "aud": f"{url.scheme}://{url.netloc}", + "exp": int(vapid_exp.timestamp()), } vapid = Vapid.from_string(private_key=vapid_private_key) return vapid.sign(vapid_claims) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index d2f5f9d8ba5..7be8634212a 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -51,12 +51,11 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N app.middlewares.append(ban_middleware) app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int) app[KEY_LOGIN_THRESHOLD] = login_threshold + app[KEY_BAN_MANAGER] = IpBanManager(hass) async def ban_startup(app: Application) -> None: """Initialize bans when app starts up.""" - ban_manager = IpBanManager(hass) - await ban_manager.async_load() - app[KEY_BAN_MANAGER] = ban_manager + await app[KEY_BAN_MANAGER].async_load() app.on_startup.append(ban_startup) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 17646fa3ed6..5a3461b02e9 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -183,7 +183,7 @@ class Router: if not self.subscriptions.get(key): return if key in self.inflight_gets: - _LOGGER.debug("Skipping already inflight get for %s", key) + _LOGGER.debug("Skipping already in-flight get for %s", key) return self.inflight_gets.add(key) _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index f97bda7481e..917250eae79 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -10,9 +10,9 @@ from stringcase import snakecase from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, + ScannerEntity, SourceType, ) -from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 8f34e808235..2ecb9564113 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", @@ -20,7 +20,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/cs.json b/homeassistant/components/huawei_lte/translations/cs.json index 7782b2fc622..e5518d722ca 100644 --- a/homeassistant/components/huawei_lte/translations/cs.json +++ b/homeassistant/components/huawei_lte/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Nejedn\u00e1 se o za\u0159\u00edzen\u00ed Huawei LTE" + "not_huawei_lte": "Nejedn\u00e1 se o za\u0159\u00edzen\u00ed Huawei LTE", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "connection_timeout": "\u010casov\u00fd limit spojen\u00ed", @@ -15,6 +16,13 @@ }, "flow_title": "Huawei LTE: {name}", "step": { + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/huawei_lte/translations/hr.json b/homeassistant/components/huawei_lte/translations/hr.json new file mode 100644 index 00000000000..e5211ba1d01 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/hr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponovna provjera autenti\u010dnosti je uspje\u0161na" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Lozinka", + "username": "Korisni\u010dko ime" + }, + "description": "Unesite pristupne podatke za ure\u0111aj.", + "title": "Ponovno autentificirajte integraciju" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index 5bb08d626d0..d87b2bba339 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -39,7 +39,7 @@ "step": { "init": { "data": { - "name": "Nama layanan notifikasi (perubahan harus dimulai ulang)", + "name": "Nama layanan notifikasi (perubahan membutuhkan proses mulai ulang)", "recipient": "Penerima notifikasi SMS", "track_wired_clients": "Lacak klien jaringan kabel", "unauthenticated_mode": "Mode tidak diautentikasi (perubahan memerlukan pemuatan ulang)" diff --git a/homeassistant/components/huawei_lte/translations/sk.json b/homeassistant/components/huawei_lte/translations/sk.json index 5ada995aa6e..2d0f4eacf54 100644 --- a/homeassistant/components/huawei_lte/translations/sk.json +++ b/homeassistant/components/huawei_lte/translations/sk.json @@ -1,7 +1,46 @@ { "config": { + "abort": { + "not_huawei_lte": "Nie je to zariadenie Huawei LTE", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "connection_timeout": "\u010casov\u00fd limit pripojenia", + "incorrect_password": "Nespr\u00e1vne heslo", + "incorrect_username": "Nespr\u00e1vne pou\u017e\u00edvate\u013esk\u00e9 meno", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_url": "Neplatn\u00e1 adresa URL", + "login_attempts_exceeded": "Bol prekro\u010den\u00fd maxim\u00e1lny po\u010det pokusov o prihl\u00e1senie, sk\u00faste to znova nesk\u00f4r", + "response_error": "Nezn\u00e1ma chyba zo zariadenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Nakonfigurujte Huawei LTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "N\u00e1zov notifika\u010dnej slu\u017eby (zmena vy\u017eaduje re\u0161tart)", + "recipient": "Pr\u00edjemcovia upozornen\u00ed SMS" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 242c902fde5..ae6ce66fbc0 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -39,13 +39,23 @@ "2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "clock_wise": "\u0412\u044a\u0440\u0442\u0435\u043d\u0435 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430", "counter_clock_wise": "\u0412\u044a\u0440\u0442\u0435\u043d\u0435 \u043e\u0431\u0440\u0430\u0442\u043d\u043e \u043d\u0430 \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430", "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" }, "trigger_type": { + "long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "remote_double_button_long_press": "\u0418 \u0434\u0432\u0430\u0442\u0430 \"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_double_button_short_press": "\u0418 \u0434\u0432\u0430\u0442\u0430 \"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043a\u0440\u0430\u0442\u043a\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", "start": "\"{subtype}\" \u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u044a\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u043d\u043e" } }, diff --git a/homeassistant/components/hue/translations/hr.json b/homeassistant/components/hue/translations/hr.json index aa28e012caf..1fe00c2f528 100644 --- a/homeassistant/components/hue/translations/hr.json +++ b/homeassistant/components/hue/translations/hr.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "all_configured": "Sva Philips Hue \u010dvori\u0161ta su ve\u0107 konfigurirana", + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "cannot_connect": "Povezivanje nije uspjelo", + "discover_timeout": "Nije mogu\u0107e otkriti Hue \u010dvori\u0161ta", + "no_bridges": "Nisu otkrivena Philips Hue \u010dvori\u0161ta", + "unknown": "Neo\u010dekivana gre\u0161ka" + }, "error": { "linking": "Do\u0161lo je do nepoznate pogre\u0161ke u povezivanju.", "register_failed": "Registracija nije uspjela. Poku\u0161ajte ponovo" @@ -8,7 +16,8 @@ "init": { "data": { "host": "Host" - } + }, + "title": "Odaberite Hue \u010dvori\u0161te" } } } diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index be64a3b0624..3698677ef8c 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -7,7 +7,7 @@ "cannot_connect": "Impossibile connettersi", "discover_timeout": "Impossibile trovare i bridge Hue", "invalid_host": "Host non valido", - "no_bridges": "Nessun bridge di Philips Hue trovato", + "no_bridges": "Nessun bridge di Philips Hue rilevato", "not_hue_bridge": "Non \u00e8 un bridge Hue", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/hue/translations/sk.json b/homeassistant/components/hue/translations/sk.json index 10605f27ce1..520fee9e6e4 100644 --- a/homeassistant/components/hue/translations/sk.json +++ b/homeassistant/components/hue/translations/sk.json @@ -5,7 +5,9 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd hostite\u013e", "no_bridges": "Neboli objaven\u00fd \u017eiaden Philips Hue bridge", + "not_hue_bridge": "Nie je to most Hue Bridge", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { @@ -14,16 +16,56 @@ }, "step": { "init": { + "data": { + "host": "Hostite\u013e" + }, "title": "Vyberte Hue bridge" }, "link": { "description": "Pre registr\u00e1ciu Philips Hue s Home Assistant stla\u010dte tla\u010didlo na Philips Hue bridge.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" + }, + "manual": { + "data": { + "host": "Hostite\u013e" + } } } }, "device_automation": { "trigger_subtype": { - "1": "Prv\u00e9 tla\u010didlo" + "1": "Prv\u00e9 tla\u010didlo", + "2": "Druh\u00e9 tla\u010didlo", + "3": "Tretie tla\u010didlo", + "4": "\u0160tvrt\u00e9 tla\u010didlo", + "button_1": "Prv\u00e9 tla\u010didlo", + "button_2": "Druh\u00e9 tla\u010didlo", + "button_3": "Tretie tla\u010didlo", + "button_4": "\u0160tvrt\u00e9 tla\u010didlo", + "double_buttons_1_3": "Prv\u00e9 a tretie tla\u010didlo", + "double_buttons_2_4": "Druh\u00e9 a \u0161tvrt\u00e9 tla\u010didlo", + "turn_off": "Vypn\u00fa\u0165", + "turn_on": "Zapn\u00fa\u0165" + }, + "trigger_type": { + "double_short_release": "Obe \u201e{subtype}\u201c boli uvo\u013enen\u00e9", + "long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_button_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_button_short_press": "\"{subtype}\" stla\u010den\u00e9", + "remote_button_short_release": "\u201c{subtype}\u201c uvo\u013enen\u00e9", + "remote_double_button_long_press": "Obe \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_double_button_short_press": "Obe \u201e{subtype}\u201c boli uvo\u013enen\u00e9", + "repeat": "\u201e{subtype}\u201c podr\u017ean\u00e9", + "short_release": "\"{subtype}\" uvo\u013enen\u00e9 po kr\u00e1tkom stla\u010den\u00ed" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Povoli\u0165 skupiny Hue", + "allow_hue_scenes": "Povoli\u0165 sc\u00e9ny Hue" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index 74fe25dafcf..e840835764b 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -108,7 +108,7 @@ def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id) if is_group: supported_color_modes = set() - supported_features = 0 + supported_features = LightEntityFeature(0) for light_id in api_item.lights: if light_id not in bridge.api.lights: continue diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index c963f366323..7c0060067c2 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -3,7 +3,11 @@ from __future__ import annotations import logging -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, POWER_WATT from homeassistant.core import HomeAssistant @@ -42,7 +46,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): user_id: str, name: str, source_type: str, - device_class: str | None = None, + device_class: SensorDeviceClass | None = None, sensor_type: str = SENSOR_TYPE_RATE, unit_of_measurement: str = POWER_WATT, icon: str = "mdi:lightning-bolt", @@ -72,7 +76,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): return self._name @property - def device_class(self) -> str | None: + def device_class(self) -> SensorDeviceClass | None: """Return the device class of the sensor.""" return self._device_class diff --git a/homeassistant/components/huisbaasje/translations/sk.json b/homeassistant/components/huisbaasje/translations/sk.json index 5ada995aa6e..0c9a112e32e 100644 --- a/homeassistant/components/huisbaasje/translations/sk.json +++ b/homeassistant/components/huisbaasje/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 1077e133b3a..61d7b6c3944 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -137,18 +137,18 @@ class HumidifierEntity(ToggleEntity): _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY _attr_mode: str | None + _attr_supported_features: HumidifierEntityFeature = HumidifierEntityFeature(0) _attr_target_humidity: int | None = None @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" - supported_features = self.supported_features or 0 data: dict[str, int | list[str] | None] = { ATTR_MIN_HUMIDITY: self.min_humidity, ATTR_MAX_HUMIDITY: self.max_humidity, } - if supported_features & HumidifierEntityFeature.MODES: + if self.supported_features & HumidifierEntityFeature.MODES: data[ATTR_AVAILABLE_MODES] = self.available_modes return data @@ -166,13 +166,12 @@ class HumidifierEntity(ToggleEntity): @property def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" - supported_features = self.supported_features or 0 data: dict[str, int | str | None] = {} if self.target_humidity is not None: data[ATTR_HUMIDITY] = self.target_humidity - if supported_features & HumidifierEntityFeature.MODES: + if self.supported_features & HumidifierEntityFeature.MODES: data[ATTR_MODE] = self.mode return data @@ -223,3 +222,8 @@ class HumidifierEntity(ToggleEntity): def max_humidity(self) -> int: """Return the maximum humidity.""" return self._attr_max_humidity + + @property + def supported_features(self) -> HumidifierEntityFeature: + """Return the list of supported features.""" + return self._attr_supported_features diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index 03f89f5489a..1f802f7fa36 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -1,5 +1,5 @@ """Provides the constants needed for component.""" -from enum import IntEnum +from enum import IntFlag MODE_NORMAL = "normal" MODE_ECO = "eco" @@ -30,7 +30,7 @@ SERVICE_SET_MODE = "set_mode" SERVICE_SET_HUMIDITY = "set_humidity" -class HumidifierEntityFeature(IntEnum): +class HumidifierEntityFeature(IntFlag): """Supported features of the alarm control panel entity.""" MODES = 1 diff --git a/homeassistant/components/humidifier/translations/is.json b/homeassistant/components/humidifier/translations/is.json new file mode 100644 index 00000000000..5009de10c9b --- /dev/null +++ b/homeassistant/components/humidifier/translations/is.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} er sl\u00f6kkt", + "is_on": "{entity_name} er \u00ed gangi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/sk.json b/homeassistant/components/humidifier/translations/sk.json new file mode 100644 index 00000000000..d9cd16a451c --- /dev/null +++ b/homeassistant/components/humidifier/translations/sk.json @@ -0,0 +1,24 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Nastavi\u0165 vlhkos\u0165 pre {entity_name}", + "turn_off": "Vypn\u00fa\u0165 {entity_name}", + "turn_on": "Zapn\u00fa\u0165 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} je vypnut\u00e9", + "is_on": "{entity_name} je zapnut\u00e9" + }, + "trigger_type": { + "changed_states": "{entity_name} zapnut\u00e9 alebo vypnut\u00e9", + "target_humidity_changed": "Cie\u013eov\u00e1 vlhkos\u0165 {entity_name} sa zmenila" + } + }, + "state": { + "_": { + "off": "Neakt\u00edvny", + "on": "Akt\u00edvny" + } + }, + "title": "Zvlh\u010dova\u010d" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 347b2c3af03..7e0206de322 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -118,7 +118,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): """Representation of a powerview shade.""" _attr_device_class = CoverDeviceClass.SHADE - _attr_supported_features = 0 + _attr_supported_features = CoverEntityFeature(0) def __init__( self, diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 15b10dca0e0..568a6ede0b1 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==2.0.3"], + "requirements": ["aiopvapi==2.0.4"], "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/sk.json b/homeassistant/components/hunterdouglas_powerview/translations/sk.json new file mode 100644 index 00000000000..bd0a4fe1d64 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({host})", + "step": { + "link": { + "description": "Chcete nastavi\u0165 {name} ({host})?" + }, + "user": { + "data": { + "host": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/sk.json b/homeassistant/components/hvv_departures/translations/sk.json index 5ada995aa6e..bacfbd506d5 100644 --- a/homeassistant/components/hvv_departures/translations/sk.json +++ b/homeassistant/components/hvv_departures/translations/sk.json @@ -1,7 +1,44 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "no_results": "\u017diadne v\u00fdsledky. Sk\u00faste pou\u017ei\u0165 in\u00fa stanicu/adresu" + }, + "step": { + "station": { + "data": { + "station": "Stanica/Adresa" + }, + "title": "Zadajte stanicu/adresu" + }, + "station_select": { + "data": { + "station": "Stanica/Adresa" + }, + "title": "Zvo\u013ete Stanica/Adresa" + }, + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripojte sa k HVV API" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "offset": "Posun (v min\u00fatach)" + }, + "title": "Mo\u017enosti" + } } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/bg.json b/homeassistant/components/hyperion/translations/bg.json index 38499f1fefa..8a0677c63ab 100644 --- a/homeassistant/components/hyperion/translations/bg.json +++ b/homeassistant/components/hyperion/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/hyperion/translations/sk.json b/homeassistant/components/hyperion/translations/sk.json index a8223b5b2e3..270b19bdc1a 100644 --- a/homeassistant/components/hyperion/translations/sk.json +++ b/homeassistant/components/hyperion/translations/sk.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "auth_new_token_not_granted_error": "Novovytvoren\u00fd token nebol schv\u00e1len\u00fd v pou\u017e\u00edvate\u013eskom rozhran\u00ed Hyperion", + "auth_new_token_not_work_error": "Nepodarilo sa overi\u0165 pomocou novovytvoren\u00e9ho tokenu", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token" + }, "step": { + "auth": { + "data": { + "create_token": "Automaticky vytvori\u0165 nov\u00fd token", + "token": "Alebo poskytnite u\u017e existuj\u00faci token" + } + }, + "create_token": { + "title": "Automaticky vytvori\u0165 nov\u00fd autentifika\u010dn\u00fd token" + }, + "create_token_external": { + "title": "Prijmite nov\u00fd token v pou\u017e\u00edvate\u013eskom rozhran\u00ed Hyperion" + }, "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/sk.json b/homeassistant/components/ialarm/translations/sk.json index 892b8b2cd91..e3d2afb6266 100644 --- a/homeassistant/components/ialarm/translations/sk.json +++ b/homeassistant/components/ialarm/translations/sk.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 91ca64a87e6..00c9445a3b5 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -85,7 +85,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): @property def effect_list(self) -> list: """Return supported light effects.""" - return list(self.dev.supported_light_effects) + return list(self.dev.supported_effects) @property def color_mode(self) -> ColorMode: @@ -100,9 +100,9 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): return {self.color_mode} @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Return the list of features supported by the light.""" if self.dev.supports_effect: return LightEntityFeature.EFFECT - return 0 + return LightEntityFeature(0) diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 1c567b04a7f..f1636a09c90 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -54,7 +54,7 @@ class HassAqualinkSensor(AqualinkEntity, SensorEntity): return float(self.dev.state) @property - def device_class(self) -> str | None: + def device_class(self) -> SensorDeviceClass | None: """Return the class of the sensor.""" if self.dev.name.endswith("_temp"): return SensorDeviceClass.TEMPERATURE diff --git a/homeassistant/components/iaqualink/translations/bg.json b/homeassistant/components/iaqualink/translations/bg.json index d6a28310dd1..07af4cdf9a3 100644 --- a/homeassistant/components/iaqualink/translations/bg.json +++ b/homeassistant/components/iaqualink/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/iaqualink/translations/sk.json b/homeassistant/components/iaqualink/translations/sk.json index 5ada995aa6e..df490c1220b 100644 --- a/homeassistant/components/iaqualink/translations/sk.json +++ b/homeassistant/components/iaqualink/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte pou\u017e\u00edvate\u013esk\u00e9 meno a heslo pre v\u00e1\u0161 \u00fa\u010det iAqualink.", + "title": "Pripojte sa k iAqualink" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 6986a26e0b0..2e9af4ad9e6 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -62,7 +62,7 @@ def async_name( """Return a name for the device.""" if service_info.address in ( service_info.name, - service_info.name.replace("_", ":"), + service_info.name.replace("-", ":"), ): base_name = f"{ibeacon_advertisement.uuid}_{ibeacon_advertisement.major}_{ibeacon_advertisement.minor}" else: @@ -355,8 +355,8 @@ class IBeaconCoordinator: if group_id not in self._unavailable_group_ids and (service_info := self._last_seen_by_group_id.get(group_id)) and ( - # We will not be callbacks for iBeacons with random macs - # that rotate infrequently since their advertisement data is + # We will not get callbacks for iBeacons with random macs + # that rotate infrequently since their advertisement data # does not change as the bluetooth.async_register_callback API # suppresses callbacks for duplicate advertisements to avoid # exposing integrations to the firehose of bluetooth advertisements. diff --git a/homeassistant/components/ibeacon/translations/cs.json b/homeassistant/components/ibeacon/translations/cs.json new file mode 100644 index 00000000000..32cf458b0af --- /dev/null +++ b/homeassistant/components/ibeacon/translations/cs.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minim\u00e1ln\u00ed RSSI" + }, + "description": "iBeacons s hodnotou RSSI ni\u017e\u0161\u00ed ne\u017e Minim\u00e1ln\u00ed RSSI budou ignorov\u00e1ny. Pokud integrace vid\u00ed sousedn\u00ed iBeacony, zv\u00fd\u0161en\u00ed t\u00e9to hodnoty m\u016f\u017ee pomoci." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/he.json b/homeassistant/components/ibeacon/translations/he.json index d0c3523da94..04f865d0d69 100644 --- a/homeassistant/components/ibeacon/translations/he.json +++ b/homeassistant/components/ibeacon/translations/he.json @@ -1,7 +1,22 @@ { "config": { "abort": { + "bluetooth_not_available": "\u05d9\u05e9 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05dc\u05e4\u05d7\u05d5\u05ea \u05de\u05ea\u05d0\u05dd Bluetooth \u05d0\u05d5 \u05e9\u05dc\u05d8 \u05e8\u05d7\u05d5\u05e7 \u05d0\u05d7\u05d3 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1-iBeacon Tracker.", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05de\u05e2\u05e7\u05d1 iBeacon?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/sk.json b/homeassistant/components/ibeacon/translations/sk.json new file mode 100644 index 00000000000..c294bc45d7c --- /dev/null +++ b/homeassistant/components/ibeacon/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index fc1de213a69..d9bd215d2a1 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index fb3d0a4ce48..92b2550d51d 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -9,11 +9,6 @@ "validate_verification_code": "\u041f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0430\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043a\u043e\u0434\u0430 \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" }, "step": { - "reauth": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - } - }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json index 187617b23e7..4205e80233c 100644 --- a/homeassistant/components/icloud/translations/ca.json +++ b/homeassistant/components/icloud/translations/ca.json @@ -11,13 +11,6 @@ "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, torna-ho a provar" }, "step": { - "reauth": { - "data": { - "password": "Contrasenya" - }, - "description": "La contrasenya introdu\u00efda anteriorment per a {username} ja no funciona. Actualitza la contrasenya per continuar utilitzant aquesta integraci\u00f3.", - "title": "Reautenticaci\u00f3 de la integraci\u00f3" - }, "reauth_confirm": { "data": { "password": "Contrasenya" diff --git a/homeassistant/components/icloud/translations/cs.json b/homeassistant/components/icloud/translations/cs.json index 72dc892d15f..5103dfcff7f 100644 --- a/homeassistant/components/icloud/translations/cs.json +++ b/homeassistant/components/icloud/translations/cs.json @@ -11,13 +11,6 @@ "validate_verification_code": "Nepoda\u0159ilo se ov\u011b\u0159it v\u00e1\u0161 ov\u011b\u0159ovac\u00ed k\u00f3d, vyberte d\u016fv\u011bryhodn\u00e9 za\u0159\u00edzen\u00ed a spus\u0165te ov\u011b\u0159en\u00ed znovu" }, "step": { - "reauth": { - "data": { - "password": "Heslo" - }, - "description": "Va\u0161e zadan\u00e9 heslo pro {username} ji\u017e nefunguje. Chcete-li tuto d\u00e1le integraci pou\u017e\u00edvat, aktualizujte sv\u00e9 heslo.", - "title": "Znovu ov\u011b\u0159it integraci" - }, "reauth_confirm": { "data": { "password": "Heslo" diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index 4d9c0d63d0c..db18c846934 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -11,13 +11,6 @@ "validate_verification_code": "Verifizierung des Verifizierungscodes fehlgeschlagen. W\u00e4hle ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starte die Verifizierung erneut" }, "step": { - "reauth": { - "data": { - "password": "Passwort" - }, - "description": "Dein zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisiere dein Passwort, um diese Integration weiterhin zu verwenden.", - "title": "Integration erneut authentifizieren" - }, "reauth_confirm": { "data": { "password": "Passwort" diff --git a/homeassistant/components/icloud/translations/el.json b/homeassistant/components/icloud/translations/el.json index d47a4349648..32067114dd4 100644 --- a/homeassistant/components/icloud/translations/el.json +++ b/homeassistant/components/icloud/translations/el.json @@ -11,13 +11,6 @@ "validate_verification_code": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac" }, "step": { - "reauth": { - "data": { - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03af\u03c7\u03b1\u03c4\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", - "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" - }, "reauth_confirm": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json index 0052a858ede..65a8892c480 100644 --- a/homeassistant/components/icloud/translations/en.json +++ b/homeassistant/components/icloud/translations/en.json @@ -11,13 +11,6 @@ "validate_verification_code": "Failed to verify your verification code, try again" }, "step": { - "reauth": { - "data": { - "password": "Password" - }, - "description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.", - "title": "Reauthenticate Integration" - }, "reauth_confirm": { "data": { "password": "Password" diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index ef1e6804469..9aa38aabd65 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -11,13 +11,6 @@ "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, int\u00e9ntalo de nuevo" }, "step": { - "reauth": { - "data": { - "password": "Contrase\u00f1a" - }, - "description": "La contrase\u00f1a introducida anteriormente para {username} ya no funciona. Actualiza tu contrase\u00f1a para seguir usando esta integraci\u00f3n.", - "title": "Volver a autenticar la integraci\u00f3n" - }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a" diff --git a/homeassistant/components/icloud/translations/et.json b/homeassistant/components/icloud/translations/et.json index 686205f3572..3fecdbefdcd 100644 --- a/homeassistant/components/icloud/translations/et.json +++ b/homeassistant/components/icloud/translations/et.json @@ -11,13 +11,6 @@ "validate_verification_code": "Tuvastuskoodi kinnitamine nurjus. Vali usaldusseade ja proovi uuesti" }, "step": { - "reauth": { - "data": { - "password": "Salas\u00f5na" - }, - "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskenda oma salas\u00f5na.", - "title": "iCloudi tuvastusandmed" - }, "reauth_confirm": { "data": { "password": "Salas\u00f5na" diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json index dec1bbdb34a..3d268fabe3b 100644 --- a/homeassistant/components/icloud/translations/fr.json +++ b/homeassistant/components/icloud/translations/fr.json @@ -11,13 +11,6 @@ "validate_verification_code": "Impossible de v\u00e9rifier votre code de v\u00e9rification, choisissez un appareil de confiance et recommencez la v\u00e9rification" }, "step": { - "reauth": { - "data": { - "password": "Mot de passe" - }, - "description": "Votre mot de passe pr\u00e9c\u00e9demment saisi pour {username} ne fonctionne plus. Mettez \u00e0 jour votre mot de passe pour continuer \u00e0 utiliser cette int\u00e9gration.", - "title": "R\u00e9-authentifier l'int\u00e9gration" - }, "reauth_confirm": { "data": { "password": "Mot de passe" diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json index eae7fa97a83..1bb30db4a0a 100644 --- a/homeassistant/components/icloud/translations/he.json +++ b/homeassistant/components/icloud/translations/he.json @@ -8,13 +8,6 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { - "reauth": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d4\u05d6\u05e0\u05ea \u05d1\u05e2\u05d1\u05e8 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05e4\u05d5\u05e2\u05dc\u05ea \u05e2\u05d5\u05d3. \u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4.", - "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" - }, "reauth_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" @@ -23,10 +16,10 @@ }, "trusted_device": { "data": { - "trusted_device": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df" + "trusted_device": "\u05d4\u05ea\u05e7\u05df \u05de\u05d4\u05d9\u05de\u05df" }, - "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05d4\u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc\u05da", - "title": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc iCloud" + "description": "\u05d9\u05e9 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc\u05da", + "title": "\u05d4\u05ea\u05e7\u05df \u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc iCloud" }, "user": { "data": { diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index 539b3740e24..177a3106a74 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -11,13 +11,6 @@ "validate_verification_code": "Nem siker\u00fclt hiteles\u00edteni az ellen\u0151rz\u0151 k\u00f3dot, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra" }, "step": { - "reauth": { - "data": { - "password": "Jelsz\u00f3" - }, - "description": "{username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", - "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" - }, "reauth_confirm": { "data": { "password": "Jelsz\u00f3" diff --git a/homeassistant/components/icloud/translations/id.json b/homeassistant/components/icloud/translations/id.json index 1f6ed7c84c9..cd8a348ce98 100644 --- a/homeassistant/components/icloud/translations/id.json +++ b/homeassistant/components/icloud/translations/id.json @@ -11,13 +11,6 @@ "validate_verification_code": "Gagal memverifikasi kode verifikasi Anda, coba lagi" }, "step": { - "reauth": { - "data": { - "password": "Kata Sandi" - }, - "description": "Kata sandi yang Anda masukkan sebelumnya untuk {username} tidak lagi berfungsi. Perbarui kata sandi Anda untuk tetap menggunakan integrasi ini.", - "title": "Autentikasi Ulang Integrasi" - }, "reauth_confirm": { "data": { "password": "Kata Sandi" diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 856ed30d767..ff91140e180 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -11,13 +11,6 @@ "validate_verification_code": "Impossibile verificare il codice di verifica, riprova" }, "step": { - "reauth": { - "data": { - "password": "Password" - }, - "description": "La password inserita in precedenza per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.", - "title": "Autentica nuovamente l'integrazione" - }, "reauth_confirm": { "data": { "password": "Password" diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 4d9230ec150..1010a6c80f6 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -11,13 +11,6 @@ "validate_verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306e\u78ba\u8a8d\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u518d\u5ea6\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" }, "step": { - "reauth": { - "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" - }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" diff --git a/homeassistant/components/icloud/translations/ka.json b/homeassistant/components/icloud/translations/ka.json index 950b4e2f327..949e95dec34 100644 --- a/homeassistant/components/icloud/translations/ka.json +++ b/homeassistant/components/icloud/translations/ka.json @@ -2,15 +2,6 @@ "config": { "abort": { "reauth_successful": "\u10e0\u10d0-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0 \u10d8\u10e7\u10dd \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8" - }, - "step": { - "reauth": { - "data": { - "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8" - }, - "description": "\u10e8\u10d4\u10dc\u10d8 \u10d0\u10d3\u10e0\u10d4 \u10e8\u10d4\u10e7\u10d5\u10d0\u10dc\u10d8\u10da\u10d8 \u10de\u10d0\u10e0\u10dd\u10da\u10d8 {username} \u10d0\u10e6\u10d0\u10e0 \u10db\u10e3\u10e8\u10d0\u10dd\u10d1\u10e1. \u10d2\u10d0\u10dc\u10d0\u10d0\u10ee\u10da\u10d4\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 \u10de\u10d0\u10e0\u10dd\u10da\u10d8, \u10e0\u10dd\u10db \u10d2\u10d0\u10dc\u10d0\u10d2\u10e0\u10eb\u10dd\u10d7 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d7 \u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10da\u10dd\u10d1\u10d0.", - "title": "\u10e0\u10d4\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d0" - } } } } \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ko.json b/homeassistant/components/icloud/translations/ko.json index 52319b888ca..fcc89a25722 100644 --- a/homeassistant/components/icloud/translations/ko.json +++ b/homeassistant/components/icloud/translations/ko.json @@ -11,13 +11,6 @@ "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc \ud655\uc778\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" }, "step": { - "reauth": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uc774\uc804\uc5d0 \uc785\ub825\ud55c {username}\uc5d0 \ub300\ud55c \ube44\ubc00\ubc88\ud638\uac00 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uacc4\uc18d \uc0ac\uc6a9\ud558\ub824\uba74 \ube44\ubc00\ubc88\ud638\ub97c \uc5c5\ub370\uc774\ud2b8\ud574\uc8fc\uc138\uc694.", - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" - }, "trusted_device": { "data": { "trusted_device": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" diff --git a/homeassistant/components/icloud/translations/lb.json b/homeassistant/components/icloud/translations/lb.json index 6f0caa00528..77c9b0c7c30 100644 --- a/homeassistant/components/icloud/translations/lb.json +++ b/homeassistant/components/icloud/translations/lb.json @@ -11,13 +11,6 @@ "validate_verification_code": "Feeler beim iwwerpr\u00e9iwe vum Verifikatiouns Code, wielt ee vertrauten Apparat aus a start d'Iwwerpr\u00e9iwung nei" }, "step": { - "reauth": { - "data": { - "password": "Passwuert" - }, - "description": "D\u00e4in Passwuert fir {username} funktionn\u00e9iert net m\u00e9i. Aktualis\u00e9ier d\u00e4in Passwuert fir d\u00ebs Integratioun weider ze benotzen.", - "title": "Integratioun re-authentifiz\u00e9ieren" - }, "trusted_device": { "data": { "trusted_device": "Vertrauten Apparat" diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 6c831c2f65b..e163942597c 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -11,13 +11,6 @@ "validate_verification_code": "Kan uw verificatiecode niet verifi\u00ebren, kies een vertrouwensapparaat en start de verificatie opnieuw" }, "step": { - "reauth": { - "data": { - "password": "Wachtwoord" - }, - "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken.", - "title": "Integratie herauthenticeren" - }, "reauth_confirm": { "data": { "password": "Wachtwoord" diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index 1423f117126..83003dfd4ca 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -11,13 +11,6 @@ "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden, pr\u00f8v p\u00e5 nytt" }, "step": { - "reauth": { - "data": { - "password": "Passord" - }, - "description": "Ditt tidligere angitte passord for {username} fungerer ikke lenger. Oppdater passordet ditt for \u00e5 fortsette \u00e5 bruke denne integrasjonen.", - "title": "Godkjenne integrering p\u00e5 nytt" - }, "reauth_confirm": { "data": { "password": "Passord" diff --git a/homeassistant/components/icloud/translations/pl.json b/homeassistant/components/icloud/translations/pl.json index a726cd5a78d..303caa918d8 100644 --- a/homeassistant/components/icloud/translations/pl.json +++ b/homeassistant/components/icloud/translations/pl.json @@ -11,13 +11,6 @@ "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, spr\u00f3buj ponownie" }, "step": { - "reauth": { - "data": { - "password": "Has\u0142o" - }, - "description": "Twoje poprzednio wprowadzone has\u0142o dla {username} ju\u017c nie dzia\u0142a. Zaktualizuj swoje has\u0142o, aby nadal korzysta\u0107 z tej integracji.", - "title": "Ponownie uwierzytelnij integracj\u0119" - }, "reauth_confirm": { "data": { "password": "Has\u0142o" diff --git a/homeassistant/components/icloud/translations/pt-BR.json b/homeassistant/components/icloud/translations/pt-BR.json index 99f779e9ead..c431ce4f26a 100644 --- a/homeassistant/components/icloud/translations/pt-BR.json +++ b/homeassistant/components/icloud/translations/pt-BR.json @@ -11,13 +11,6 @@ "validate_verification_code": "Falha ao verificar seu c\u00f3digo de verifica\u00e7\u00e3o, tente novamente" }, "step": { - "reauth": { - "data": { - "password": "Senha" - }, - "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o.", - "title": "Reautenticar Integra\u00e7\u00e3o" - }, "reauth_confirm": { "data": { "password": "Senha" diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index da7711298fc..7eb0b4f4498 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -8,13 +8,6 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { - "reauth": { - "data": { - "password": "Palavra-passe" - }, - "description": "A sua palavra-passe anteriormente introduzida para {username} j\u00e1 n\u00e3o \u00e9 v\u00e1lida. Atualize sua palavra-passe para continuar a utilizar esta integra\u00e7\u00e3o.", - "title": "Reautenticar integra\u00e7\u00e3o" - }, "reauth_confirm": { "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o." }, diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index 17acded5703..5860f2a096c 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -11,13 +11,6 @@ "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." }, "step": { - "reauth": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u0420\u0430\u043d\u0435\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" - }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" diff --git a/homeassistant/components/icloud/translations/sk.json b/homeassistant/components/icloud/translations/sk.json index d30ed436a4f..fbd317aec47 100644 --- a/homeassistant/components/icloud/translations/sk.json +++ b/homeassistant/components/icloud/translations/sk.json @@ -1,16 +1,41 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "send_verification_code": "Odoslanie overovacieho k\u00f3du zlyhalo", + "validate_verification_code": "Nepodarilo sa overi\u0165 overovac\u00ed k\u00f3d, sk\u00faste to znova" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "trusted_device": { + "data": { + "trusted_device": "D\u00f4veryhodn\u00e9 zariadenie" + }, + "description": "Vyberte svoje d\u00f4veryhodn\u00e9 zariadenie", + "title": "d\u00f4veryhodn\u00e9 zariadenie iCloud" + }, "user": { "data": { + "password": "Heslo", "username": "Email" - } + }, + "title": "iCloud poverenia" + }, + "verification_code": { + "data": { + "verification_code": "Overovac\u00ed k\u00f3d" + }, + "description": "Zadajte overovac\u00ed k\u00f3d, ktor\u00fd ste pr\u00e1ve dostali z iCloud", + "title": "iCloud overovac\u00ed k\u00f3d" } } } diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index b76a5408319..b3fd0149cc4 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -11,13 +11,6 @@ "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen" }, "step": { - "reauth": { - "data": { - "password": "L\u00f6senord" - }, - "description": "Ditt tidigare angivna l\u00f6senord f\u00f6r {username} fungerar inte l\u00e4ngre. Uppdatera ditt l\u00f6senord f\u00f6r att forts\u00e4tta anv\u00e4nda denna integration.", - "title": "\u00c5terautenticera integration" - }, "reauth_confirm": { "data": { "password": "L\u00f6senord" diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index e220141f24d..35540db8526 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -11,13 +11,6 @@ "validate_verification_code": "Do\u011frulama kodunuz do\u011frulanamad\u0131, tekrar deneyin" }, "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.", - "title": "Entegrasyonu Yeniden Do\u011frula" - }, "reauth_confirm": { "data": { "password": "Parola" diff --git a/homeassistant/components/icloud/translations/uk.json b/homeassistant/components/icloud/translations/uk.json index ac65157f050..cd416b8058c 100644 --- a/homeassistant/components/icloud/translations/uk.json +++ b/homeassistant/components/icloud/translations/uk.json @@ -11,13 +11,6 @@ "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" diff --git a/homeassistant/components/icloud/translations/zh-Hans.json b/homeassistant/components/icloud/translations/zh-Hans.json index 96b16faec97..bd57ef72fb7 100644 --- a/homeassistant/components/icloud/translations/zh-Hans.json +++ b/homeassistant/components/icloud/translations/zh-Hans.json @@ -8,11 +8,6 @@ "validate_verification_code": "\u65e0\u6cd5\u9a8c\u8bc1\u9a8c\u8bc1\u7801\uff0c\u8bf7\u9009\u62e9\u53d7\u4fe1\u4efb\u7684\u8bbe\u5907\u5e76\u91cd\u65b0\u5f00\u59cb\u9a8c\u8bc1" }, "step": { - "reauth": { - "data": { - "password": "\u5bc6\u7801" - } - }, "trusted_device": { "data": { "trusted_device": "\u53d7\u4fe1\u4efb\u7684\u8bbe\u5907" diff --git a/homeassistant/components/icloud/translations/zh-Hant.json b/homeassistant/components/icloud/translations/zh-Hant.json index 91f14636dd2..5231889afdb 100644 --- a/homeassistant/components/icloud/translations/zh-Hant.json +++ b/homeassistant/components/icloud/translations/zh-Hant.json @@ -11,13 +11,6 @@ "validate_verification_code": "\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" }, "step": { - "reauth": { - "data": { - "password": "\u5bc6\u78bc" - }, - "description": "\u5148\u524d\u91dd\u5c0d\u5e33\u865f {username} \u6240\u8f38\u5165\u7684\u5bc6\u78bc\u5df2\u5931\u6548\u3002\u8acb\u66f4\u65b0\u5bc6\u78bc\u4ee5\u4f7f\u7528\u6b64\u6574\u5408\u3002", - "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" - }, "reauth_confirm": { "data": { "password": "\u5bc6\u78bc" diff --git a/homeassistant/components/ifttt/translations/hr.json b/homeassistant/components/ifttt/translations/hr.json new file mode 100644 index 00000000000..9b1af1faf2d --- /dev/null +++ b/homeassistant/components/ifttt/translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Jeste li sigurni da \u017eelite postaviti IFTTT?", + "title": "Postavljanje IFTTT Webhook apleta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/sk.json b/homeassistant/components/ifttt/translations/sk.json new file mode 100644 index 00000000000..97f8b46cbdf --- /dev/null +++ b/homeassistant/components/ifttt/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + }, + "step": { + "user": { + "description": "Naozaj chcete nastavi\u0165 IFTTT?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index 97836e7f145..927e52f594d 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -5,5 +5,6 @@ "requirements": ["georss_ign_sismologia_client==0.3"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["georss_ign_sismologia_client"] + "loggers": ["georss_ign_sismologia_client"], + "integration_type": "service" } diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 51263e38ab7..23ab393aabd 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -6,6 +6,7 @@ import logging import pathlib import secrets import shutil +from typing import Any from PIL import Image, ImageOps, UnidentifiedImageError from aiohttp import hdrs, web @@ -71,7 +72,7 @@ class ImageStorageCollection(collection.StorageCollection): self.async_add_listener(self._change_listener) self.image_dir = image_dir - async def _process_create_data(self, data: dict) -> dict: + async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: """Validate the config is valid.""" data = self.CREATE_SCHEMA(dict(data)) uploaded_file: FileField = data["file"] @@ -88,7 +89,7 @@ class ImageStorageCollection(collection.StorageCollection): return data - def _move_data(self, data): + def _move_data(self, data: dict[str, Any]) -> int: """Move data.""" uploaded_file: FileField = data.pop("file") @@ -119,15 +120,24 @@ class ImageStorageCollection(collection.StorageCollection): return media_file.stat().st_size @callback - def _get_suggested_id(self, info: dict) -> str: + def _get_suggested_id(self, info: dict[str, Any]) -> str: """Suggest an ID based on the config.""" - return info[CONF_ID] + return str(info[CONF_ID]) - async def _update_data(self, data: dict, update_data: dict) -> dict: + async def _update_data( + self, + data: dict[str, Any], + update_data: dict[str, Any], + ) -> dict[str, Any]: """Return a new updated data object.""" return {**data, **self.UPDATE_SCHEMA(update_data)} - async def _change_listener(self, change_type, item_id, data): + async def _change_listener( + self, + change_type: str, + item_id: str, + data: dict[str, Any], + ) -> None: """Handle change.""" if change_type != collection.CHANGE_REMOVED: return @@ -141,7 +151,7 @@ class ImageUploadView(HomeAssistantView): url = "/api/image/upload" name = "api:image:upload" - async def post(self, request): + async def post(self, request: web.Request) -> web.Response: """Handle upload.""" # Increase max payload request._client_max_size = MAX_SIZE # pylint: disable=protected-access @@ -159,26 +169,27 @@ class ImageServeView(HomeAssistantView): requires_auth = False def __init__( - self, image_folder: pathlib.Path, image_collection: ImageStorageCollection + self, + image_folder: pathlib.Path, + image_collection: ImageStorageCollection, ) -> None: """Initialize image serve view.""" self.transform_lock = asyncio.Lock() self.image_folder = image_folder self.image_collection = image_collection - async def get(self, request: web.Request, image_id: str, filename: str): + async def get( + self, + request: web.Request, + image_id: str, + filename: str, + ) -> web.FileResponse: """Serve image.""" - image_size = filename.split("-", 1)[0] try: - parts = image_size.split("x", 1) - width = int(parts[0]) - height = int(parts[1]) + width, height = _validate_size_from_filename(filename) except (ValueError, IndexError) as err: raise web.HTTPBadRequest from err - if not width or width != height or width not in VALID_SIZES: - raise web.HTTPBadRequest - image_info = self.image_collection.data.get(image_id) if image_info is None: @@ -205,8 +216,33 @@ class ImageServeView(HomeAssistantView): ) -def _generate_thumbnail(original_path, content_type, target_path, target_size): +def _generate_thumbnail( + original_path: pathlib.Path, + content_type: str, + target_path: pathlib.Path, + target_size: tuple[int, int], +) -> None: """Generate a size.""" image = ImageOps.exif_transpose(Image.open(original_path)) image.thumbnail(target_size) - image.save(target_path, format=content_type.split("/", 1)[1]) + image.save(target_path, format=content_type.partition("/")[-1]) + + +def _validate_size_from_filename(filename: str) -> tuple[int, int]: + """Parse image size from the given filename (of the form WIDTHxHEIGHT-filename). + + >>> _validate_size_from_filename("100x100-image.png") + (100, 100) + >>> _validate_size_from_filename("jeff.png") + Traceback (most recent call last): + ... + """ + image_size = filename.partition("-")[0] + if not image_size: + raise ValueError("Invalid filename") + width_s, _, height_s = image_size.partition("x") + width = int(width_s) + height = int(height_s) + if not width or width != height or width not in VALID_SIZES: + raise ValueError(f"Invalid size {image_size}") + return (width, height) diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index ed500c89011..888d6fc1fab 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==9.2.0"], + "requirements": ["pillow==9.3.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 733e7d8adc2..fb16cc48f8b 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -88,11 +88,6 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): """Return the unit of measurement.""" return TEMP_CELSIUS - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return 0 - @property def current_operation(self) -> str: """Return the current operation mode.""" diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 71d6f00ea40..d0e06e81647 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from inkbird_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -20,16 +20,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN @@ -72,27 +69,13 @@ def _device_key_to_bluetooth_entity_key( return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/inkbird/translations/he.json b/homeassistant/components/inkbird/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/inkbird/translations/he.json +++ b/homeassistant/components/inkbird/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/inkbird/translations/sk.json b/homeassistant/components/inkbird/translations/sk.json new file mode 100644 index 00000000000..b121bbc35a3 --- /dev/null +++ b/homeassistant/components/inkbird/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index ffbad247632..798dba41984 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Verbindung fehlgeschlagen", - "not_insteon_device": "Erkanntes Ger\u00e4t ist kein Insteon-Ger\u00e4t", + "not_insteon_device": "Erkanntes Ger\u00e4t ist kein Insteon Ger\u00e4t", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index c5bd0f1af87..38a04fa1fe0 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Impossibile connettersi", - "not_insteon_device": "Dispositivo rilevato non \u00e8 un dispositivo Insteon", + "not_insteon_device": "Il dispositivo rilevato non \u00e8 un dispositivo Insteon", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { diff --git a/homeassistant/components/insteon/translations/sk.json b/homeassistant/components/insteon/translations/sk.json index b3711644c03..651f7012660 100644 --- a/homeassistant/components/insteon/translations/sk.json +++ b/homeassistant/components/insteon/translations/sk.json @@ -1,26 +1,75 @@ { "config": { "abort": { - "cannot_connect": "Nepodarilo sa pripoji\u0165" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "select_single": "Vyberte jednu mo\u017enos\u0165." + }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Chcete nastavi\u0165 {name}?" + }, "hubv1": { "data": { + "host": "IP adresa", "port": "Port" } }, "hubv2": { "data": { - "port": "Port" + "host": "IP adresa", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "plm": { + "data": { + "device": "Cesta k zariadeniu USB" } } } }, "options": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "input_error": "Neplatn\u00e9 polo\u017eky, skontrolujte svoje hodnoty.", + "select_single": "Vyberte jednu mo\u017enos\u0165." + }, "step": { + "add_override": { + "data": { + "address": "Adresa zariadenia (t. j. 1a2b3c)", + "cat": "Kateg\u00f3ria zariadenia (t. j. 0x10)", + "subcat": "Podkateg\u00f3ria zariadenia (napr. 0x0a)" + } + }, + "add_x10": { + "data": { + "housecode": "K\u00f3d domu (a - p)", + "platform": "Platforma" + } + }, "change_hub_config": { "data": { - "port": "Port" + "host": "IP adresa", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "remove_override": { + "data": { + "address": "Vyberte adresu zariadenia, ktor\u00fa chcete odstr\u00e1ni\u0165" + } + }, + "remove_x10": { + "data": { + "address": "Vyberte adresu zariadenia, ktor\u00fa chcete odstr\u00e1ni\u0165" } } } diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index aab5671f64b..446132ece7c 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -6,6 +6,7 @@ from typing import Any, cast import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_METHOD, CONF_NAME, @@ -18,7 +19,6 @@ from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaConfigFlowHandler, SchemaFlowFormStep, - SchemaFlowMenuStep, ) from .const import ( @@ -65,7 +65,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): selector.TextSelector(), vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector( - selector.EntitySelectorConfig(domain="sensor") + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) ), vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.SelectSelector( selector.SelectSelectorConfig(options=INTEGRATION_METHODS), @@ -89,12 +89,12 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "user": SchemaFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA), } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), } diff --git a/homeassistant/components/integration/translations/de.json b/homeassistant/components/integration/translations/de.json index af26ec446a2..c01af414971 100644 --- a/homeassistant/components/integration/translations/de.json +++ b/homeassistant/components/integration/translations/de.json @@ -16,7 +16,7 @@ "unit_time": "Die Ausgabe wird entsprechend der gew\u00e4hlten Zeiteinheit skaliert." }, "description": "Erstelle einen Sensor, der eine Riemann-Summe berechnet, um das Integral eines Sensors zu sch\u00e4tzen.", - "title": "Riemann-Summenintegralsensor hinzuf\u00fcgen" + "title": "Riemann Summenintegralsensor hinzuf\u00fcgen" } } }, @@ -32,5 +32,5 @@ } } }, - "title": "Integration - Riemann-Summenintegralsensor" + "title": "Integration - Riemann Summenintegralsensor" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/sk.json b/homeassistant/components/integration/translations/sk.json index c1372b00a8a..25cf5102677 100644 --- a/homeassistant/components/integration/translations/sk.json +++ b/homeassistant/components/integration/translations/sk.json @@ -2,6 +2,17 @@ "config": { "step": { "user": { + "data": { + "name": "N\u00e1zov", + "round": "Presnos\u0165", + "source": "Vstupn\u00fd sn\u00edma\u010d" + } + } + } + }, + "options": { + "step": { + "init": { "data": { "round": "Presnos\u0165" } diff --git a/homeassistant/components/intellifire/translations/bg.json b/homeassistant/components/intellifire/translations/bg.json index 0a0b8c64e8e..af24819f4ac 100644 --- a/homeassistant/components/intellifire/translations/bg.json +++ b/homeassistant/components/intellifire/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "api_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0432\u043b\u0438\u0437\u0430\u043d\u0435", diff --git a/homeassistant/components/intellifire/translations/cs.json b/homeassistant/components/intellifire/translations/cs.json index 9ac5e9d9099..7f7a3e0bf58 100644 --- a/homeassistant/components/intellifire/translations/cs.json +++ b/homeassistant/components/intellifire/translations/cs.json @@ -5,6 +5,7 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "api_error": "P\u0159ihl\u00e1\u0161en\u00ed selhalo", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "flow_title": "{serial} ({host})", diff --git a/homeassistant/components/intellifire/translations/de.json b/homeassistant/components/intellifire/translations/de.json index d9427853cdd..2840db3a1e3 100644 --- a/homeassistant/components/intellifire/translations/de.json +++ b/homeassistant/components/intellifire/translations/de.json @@ -31,7 +31,7 @@ "data": { "host": "Host" }, - "description": "Die folgenden IntelliFire-Ger\u00e4te wurden gefunden. Bitte w\u00e4hle aus, welche du konfigurieren m\u00f6chtest.", + "description": "Die folgenden IntelliFire Ger\u00e4te wurden gefunden. Bitte w\u00e4hle aus, welche du konfigurieren m\u00f6chtest.", "title": "Ger\u00e4teauswahl" } } diff --git a/homeassistant/components/intellifire/translations/id.json b/homeassistant/components/intellifire/translations/id.json index 6a38f501811..70205984aaa 100644 --- a/homeassistant/components/intellifire/translations/id.json +++ b/homeassistant/components/intellifire/translations/id.json @@ -19,7 +19,7 @@ } }, "dhcp_confirm": { - "description": "Ingin menyiapkan {host}\nSerial: ({serial})?" + "description": "Ingin menyiapkan {host}\nSerial: {serial}?" }, "manual_device_entry": { "data": { diff --git a/homeassistant/components/intellifire/translations/sk.json b/homeassistant/components/intellifire/translations/sk.json new file mode 100644 index 00000000000..944a3c43d39 --- /dev/null +++ b/homeassistant/components/intellifire/translations/sk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "api_error": "Prihl\u00e1senie zlyhalo", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "iftapi_connect": "Chyba pri prip\u00e1jan\u00ed k iftapi.net" + }, + "flow_title": "{serial} ({host})", + "step": { + "api_config": { + "data": { + "password": "Heslo", + "username": "Email" + } + }, + "dhcp_confirm": { + "description": "Chcete nastavi\u0165 {host}\nSerial: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Hostite\u013e (IP adresa)" + }, + "description": "Lok\u00e1lna konfigur\u00e1cia" + }, + "pick_device": { + "data": { + "host": "Hostite\u013e" + }, + "title": "V\u00fdber zariadenia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index e8c5c580708..0090b1def9b 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -1,5 +1,6 @@ """Handle intents with scripts.""" import copy +import logging import voluptuous as vol @@ -8,6 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, intent, script, template from homeassistant.helpers.typing import ConfigType +_LOGGER = logging.getLogger(__name__) + DOMAIN = "intent_script" CONF_INTENTS = "intents" @@ -83,6 +86,16 @@ class ScriptIntentHandler(intent.IntentHandler): is_async_action = self.config.get(CONF_ASYNC_ACTION) slots = {key: value["value"] for key, value in intent_obj.slots.items()} + _LOGGER.debug( + "Intent named %s received with slots: %s", + intent_obj.intent_type, + { + key: value + for key, value in slots.items() + if not key.startswith("_") and not key.endswith("_raw_value") + }, + ) + if action is not None: if is_async_action: intent_obj.hass.async_create_task( diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 29cb30b81a2..bfdf406ee1a 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -172,7 +172,6 @@ class IntesisAC(ClimateEntity): self._hvane = None self._power = False self._fan_speed = None - self._attr_supported_features = 0 self._power_consumption_heat = None self._power_consumption_cool = None diff --git a/homeassistant/components/ios/translations/sk.json b/homeassistant/components/ios/translations/sk.json index e227301685b..d9f4f46bffe 100644 --- a/homeassistant/components/ios/translations/sk.json +++ b/homeassistant/components/ios/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "step": { "confirm": { "description": "Chcete za\u010da\u0165 nastavova\u0165?" diff --git a/homeassistant/components/iotawatt/translations/de.json b/homeassistant/components/iotawatt/translations/de.json index d5626c7e135..c30bb01d875 100644 --- a/homeassistant/components/iotawatt/translations/de.json +++ b/homeassistant/components/iotawatt/translations/de.json @@ -11,7 +11,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Das IoTawatt-Ger\u00e4t erfordert eine Authentifizierung. Bitte gib den Benutzernamen und das Passwort ein und dr\u00fccke auf die Schaltfl\u00e4che Senden." + "description": "Das IoTawatt Ger\u00e4t erfordert eine Authentifizierung. Bitte gib den Benutzernamen und das Passwort ein und dr\u00fccke auf die Schaltfl\u00e4che Senden." }, "user": { "data": { diff --git a/homeassistant/components/iotawatt/translations/sk.json b/homeassistant/components/iotawatt/translations/sk.json index 5ada995aa6e..0fcf434a18d 100644 --- a/homeassistant/components/iotawatt/translations/sk.json +++ b/homeassistant/components/iotawatt/translations/sk.json @@ -1,7 +1,22 @@ { "config": { "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "auth": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/sk.json b/homeassistant/components/ipma/translations/sk.json index e5a635afe10..645383285b9 100644 --- a/homeassistant/components/ipma/translations/sk.json +++ b/homeassistant/components/ipma/translations/sk.json @@ -1,10 +1,14 @@ { "config": { + "error": { + "name_exists": "N\u00e1zov u\u017e existuje" + }, "step": { "user": { "data": { "latitude": "Zemepisn\u00e1 \u0161\u00edrka", "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "mode": "Re\u017eim", "name": "N\u00e1zov" }, "title": "Umiestnenie" diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index c448fad592d..467523c5830 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -38,9 +38,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_MODE, CONF_NAME, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry @@ -115,9 +115,9 @@ async def async_setup_entry( class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR _attr_attribution = ATTRIBUTION diff --git a/homeassistant/components/ipp/translations/bg.json b/homeassistant/components/ipp/translations/bg.json index 19680bdcfb4..bac35896d94 100644 --- a/homeassistant/components/ipp/translations/bg.json +++ b/homeassistant/components/ipp/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index 96319641c6b..3c2df9a6ee0 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "Vuoi configurare {name}?", - "title": "Stampante rilevata" + "title": "Rilevata stampante" } } } diff --git a/homeassistant/components/ipp/translations/sk.json b/homeassistant/components/ipp/translations/sk.json index 892b8b2cd91..1f5c5b348ec 100644 --- a/homeassistant/components/ipp/translations/sk.json +++ b/homeassistant/components/ipp/translations/sk.json @@ -1,10 +1,29 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "ipp_error": "Vyskytla sa chyba IPP.", + "ipp_version_error": "Verzia IPP nie je podporovan\u00e1 tla\u010diar\u0148ou." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "connection_upgrade": "Nepodarilo sa pripoji\u0165 k tla\u010diarni. Sk\u00faste to znova so za\u010diarknutou mo\u017enos\u0165ou SSL/TLS." + }, + "flow_title": "{name}", "step": { "user": { "data": { - "port": "Port" - } + "host": "Hostite\u013e", + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + }, + "title": "Prepojte tla\u010diare\u0148" + }, + "zeroconf_confirm": { + "description": "Chcete nastavi\u0165 {name}?", + "title": "Objaven\u00e1 tla\u010diare\u0148" } } } diff --git a/homeassistant/components/iqvia/translations/sk.json b/homeassistant/components/iqvia/translations/sk.json new file mode 100644 index 00000000000..4d25cac41f9 --- /dev/null +++ b/homeassistant/components/iqvia/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "invalid_zip_code": "PS\u010c je neplatn\u00e9" + }, + "step": { + "user": { + "data": { + "zip_code": "PS\u010c" + }, + "description": "Vypl\u0148te PS\u010c v USA alebo Kanade." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/sk.json b/homeassistant/components/islamic_prayer_times/translations/sk.json new file mode 100644 index 00000000000..10b77b1d0aa --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete nastavi\u0165 \u010dasy islamsk\u00fdch modlitieb?", + "title": "Nastavte \u010dasy islamsk\u00fdch modlitieb" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Met\u00f3da v\u00fdpo\u010dtu modlitby" + } + } + } + }, + "title": "\u010casy islamsk\u00fdch modlitieb" +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/sk.json b/homeassistant/components/iss/translations/sk.json new file mode 100644 index 00000000000..0d6627fce15 --- /dev/null +++ b/homeassistant/components/iss/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Zemepisn\u00e1 \u0161\u00edrka a d\u013a\u017eka nie s\u00fa definovan\u00e9 v aplik\u00e1cii Home Assistant.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 23e77ba849d..81efdb5922a 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -70,7 +70,7 @@ async def async_setup_entry( | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity, ] = {} - child_nodes: list[tuple[Node, str | None, str | None]] = [] + child_nodes: list[tuple[Node, BinarySensorDeviceClass | None, str | None]] = [] entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] @@ -192,7 +192,9 @@ async def async_setup_entry( async_add_entities(entities) -def _detect_device_type_and_class(node: Group | Node) -> tuple[str | None, str | None]: +def _detect_device_type_and_class( + node: Group | Node, +) -> tuple[BinarySensorDeviceClass | None, str | None]: try: device_type = node.type except AttributeError: @@ -220,7 +222,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): def __init__( self, node: Node, - force_device_class: str | None = None, + force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, ) -> None: """Initialize the ISY994 binary sensor device.""" @@ -235,7 +237,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): return bool(self._node.status) @property - def device_class(self) -> str | None: + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this device. This was discovered by parsing the device type code during init @@ -255,7 +257,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): def __init__( self, node: Node, - force_device_class: str | None = None, + force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, ) -> None: """Initialize the ISY994 binary sensor device.""" @@ -484,7 +486,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): return bool(self._computed_state) @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Get the class of this device.""" return BinarySensorDeviceClass.BATTERY diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 7c82ea2459a..60027a31c89 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,7 +1,7 @@ """Support for ISY994 covers.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from pyisy.constants import ISY_VALUE_UNKNOWN @@ -58,7 +58,7 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): if self._node.status == ISY_VALUE_UNKNOWN: return None if self._node.uom == UOM_8_BIT_RANGE: - return round(self._node.status * 100.0 / 255.0) + return round(cast(float, self._node.status) * 100.0 / 255.0) return int(sorted((0, self._node.status, 100))[1]) @property diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 4606ef7e8de..6e67ed32938 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,7 +1,7 @@ """Support for ISY994 lights.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from pyisy.constants import ISY_VALUE_UNKNOWN from pyisy.helpers import NodeProperty @@ -70,7 +70,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): return None # Special Case for ISY Z-Wave Devices using % instead of 0-255: if self._node.uom == UOM_PERCENTAGE: - return round(self._node.status * 255.0 / 100.0) + return round(cast(float, self._node.status) * 255.0 / 100.0) return int(self._node.status) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/isy994/translations/bg.json b/homeassistant/components/isy994/translations/bg.json index bba26a6cf89..75bd10bbca9 100644 --- a/homeassistant/components/isy994/translations/bg.json +++ b/homeassistant/components/isy994/translations/bg.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index 45cd90895c7..cecdcde6bd3 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -10,6 +10,7 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, + "flow_title": "{name} ({host})", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/isy994/translations/he.json b/homeassistant/components/isy994/translations/he.json index e72724785cc..20e1da4fb79 100644 --- a/homeassistant/components/isy994/translations/he.json +++ b/homeassistant/components/isy994/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/isy994/translations/sk.json b/homeassistant/components/isy994/translations/sk.json index 5ada995aa6e..06886d7c6bc 100644 --- a/homeassistant/components/isy994/translations/sk.json +++ b/homeassistant/components/isy994/translations/sk.json @@ -1,7 +1,47 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_host": "Z\u00e1znam hostite\u013ea nemal \u00fapln\u00fd form\u00e1t adresy URL, napr. http://192.168.10.100:80", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({host})", + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Poverenia pre {host} u\u017e nie s\u00fa platn\u00e9." + }, + "user": { + "data": { + "host": "URL", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Polo\u017eka hostite\u013ea mus\u00ed by\u0165 v \u00faplnom form\u00e1te URL, napr. http://192.168.10.100:80", + "title": "Pripoji\u0165 sa k ISY" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "restore_light_state": "Obnovenie jasu osvetlenia" + } + } + } + }, + "system_health": { + "info": { + "device_connected": "ISY pripojen\u00e9", + "host_reachable": "Hostite\u013e je dostupn\u00fd" } } } \ No newline at end of file diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 31ad5f7860d..c94ccada5b3 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -445,7 +445,6 @@ class ZoneDevice(ClimateEntity): self._zone = zone self._name = zone.name.title() - self._attr_supported_features = 0 if zone.type != Zone.Type.AUTO: self._state_to_pizone = { HVACMode.OFF: Zone.Mode.CLOSE, @@ -518,7 +517,7 @@ class ZoneDevice(ClimateEntity): @property @_return_on_connection_error(0) - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" if self._zone.mode == Zone.Mode.AUTO: return self._attr_supported_features diff --git a/homeassistant/components/izone/translations/he.json b/homeassistant/components/izone/translations/he.json index 380dbc5d7fc..032c9c9fa17 100644 --- a/homeassistant/components/izone/translations/he.json +++ b/homeassistant/components/izone/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/izone/translations/sk.json b/homeassistant/components/izone/translations/sk.json new file mode 100644 index 00000000000..99798036ffd --- /dev/null +++ b/homeassistant/components/izone/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index 36fb65916d2..60fae2caac7 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -198,11 +198,11 @@ class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity): return get_artwork_url(self.coordinator.api_client, self.now_playing, 150) @property - def supported_features(self) -> int: + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" commands: list[str] = self.capabilities.get("SupportedCommands", []) controllable = self.capabilities.get("SupportsMediaControl", False) - features = 0 + features = MediaPlayerEntityFeature(0) if controllable: features |= ( diff --git a/homeassistant/components/jellyfin/translations/sk.json b/homeassistant/components/jellyfin/translations/sk.json index 1b1e671c054..02a53ae31a4 100644 --- a/homeassistant/components/jellyfin/translations/sk.json +++ b/homeassistant/components/jellyfin/translations/sk.json @@ -1,12 +1,19 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "password": "Heslo" + "password": "Heslo", + "url": "URL", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index d9e1d55afbf..b20364f7e64 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -267,7 +267,7 @@ class JewishCalendarSensor(SensorEntity): class JewishCalendarTimeSensor(JewishCalendarSensor): - """Implement attrbutes for sensors returning times.""" + """Implement attributes for sensors returning times.""" _attr_device_class = SensorDeviceClass.TIMESTAMP diff --git a/homeassistant/components/juicenet/translations/sk.json b/homeassistant/components/juicenet/translations/sk.json index 5ada995aa6e..081a4e8d77f 100644 --- a/homeassistant/components/juicenet/translations/sk.json +++ b/homeassistant/components/juicenet/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_token": "API token" + }, + "description": "Budete potrebova\u0165 API Token z https://home.juice.net/Manage.", + "title": "Pripojte sa k JuiceNet" + } } } } \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/sk.json b/homeassistant/components/justnimbus/translations/sk.json new file mode 100644 index 00000000000..1fb2f793d1f --- /dev/null +++ b/homeassistant/components/justnimbus/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "client_id": "ID klienta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/sk.json b/homeassistant/components/kaleidescape/translations/sk.json new file mode 100644 index 00000000000..0dce503bffa --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", + "unsupported": "Nepodporovan\u00e9 zariadenie" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unsupported": "Nepodporovan\u00e9 zariadenie" + }, + "flow_title": "{model} ({name})", + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 4491b644b27..fd4265a4ef0 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -7,9 +7,9 @@ from ndms2_client import Device from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, + ScannerEntity, SourceType, ) -from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry diff --git a/homeassistant/components/keenetic_ndms2/translations/sk.json b/homeassistant/components/keenetic_ndms2/translations/sk.json index 892b8b2cd91..a53f9a1b785 100644 --- a/homeassistant/components/keenetic_ndms2/translations/sk.json +++ b/homeassistant/components/keenetic_ndms2/translations/sk.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 078354c705e..96f52ef7e03 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -321,15 +321,15 @@ class KefMediaPlayer(MediaPlayerEntity): return mode = await self._speaker.get_mode() - self._dsp = dict( - desk_db=await self._speaker.get_desk_db(), - wall_db=await self._speaker.get_wall_db(), - treble_db=await self._speaker.get_treble_db(), - high_hz=await self._speaker.get_high_hz(), - low_hz=await self._speaker.get_low_hz(), - sub_db=await self._speaker.get_sub_db(), + self._dsp = { + "desk_db": await self._speaker.get_desk_db(), + "wall_db": await self._speaker.get_wall_db(), + "treble_db": await self._speaker.get_treble_db(), + "high_hz": await self._speaker.get_high_hz(), + "low_hz": await self._speaker.get_low_hz(), + "sub_db": await self._speaker.get_sub_db(), **mode._asdict(), - ) + } async def async_added_to_hass(self) -> None: """Subscribe to DSP updates.""" diff --git a/homeassistant/components/kegtron/device.py b/homeassistant/components/kegtron/device.py index b97aed76b7d..85516a3aea3 100644 --- a/homeassistant/components/kegtron/device.py +++ b/homeassistant/components/kegtron/device.py @@ -3,13 +3,11 @@ from __future__ import annotations import logging -from kegtron_ble import DeviceKey, SensorDeviceInfo +from kegtron_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -19,17 +17,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py index 892d8651185..b9386dd9bb4 100644 --- a/homeassistant/components/kegtron/sensor.py +++ b/homeassistant/components/kegtron/sensor.py @@ -26,9 +26,10 @@ from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, VOLUME_LITER from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { KegtronSensorDeviceClass.PORT_COUNT: SensorEntityDescription( @@ -85,7 +86,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/kegtron/translations/cs.json b/homeassistant/components/kegtron/translations/cs.json new file mode 100644 index 00000000000..1163b27775a --- /dev/null +++ b/homeassistant/components/kegtron/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "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", + "not_supported": "Za\u0159\u00edzen\u00ed nen\u00ed podporov\u00e1no" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/he.json b/homeassistant/components/kegtron/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/kegtron/translations/he.json +++ b/homeassistant/components/kegtron/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/kegtron/translations/sk.json b/homeassistant/components/kegtron/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/kegtron/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/cs.json b/homeassistant/components/keymitt_ble/translations/cs.json new file mode 100644 index 00000000000..f0a8f0aab5a --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/de.json b/homeassistant/components/keymitt_ble/translations/de.json index a03d9c725be..2e77e757565 100644 --- a/homeassistant/components/keymitt_ble/translations/de.json +++ b/homeassistant/components/keymitt_ble/translations/de.json @@ -16,7 +16,7 @@ "address": "Ger\u00e4teadresse", "name": "Name" }, - "title": "MicroBot-Ger\u00e4t einrichten" + "title": "MicroBot Ger\u00e4t einrichten" }, "link": { "description": "Dr\u00fccke die Taste am MicroBot Push, wenn die LED durchgehend rosa oder gr\u00fcn leuchtet, um sich bei Home Assistant zu registrieren.", diff --git a/homeassistant/components/keymitt_ble/translations/sk.json b/homeassistant/components/keymitt_ble/translations/sk.json new file mode 100644 index 00000000000..f9c4614994b --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured_device": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_unconfigured_devices": "Nena\u0161li sa \u017eiadne nenakonfigurovan\u00e9 zariadenia.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "linking": "P\u00e1rovanie zlyhalo, sk\u00faste to znova. Je MicroBot v re\u017eime p\u00e1rovania?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Adresa zariadenia", + "name": "N\u00e1zov" + } + }, + "link": { + "title": "P\u00e1rovanie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/sk.json b/homeassistant/components/kmtronic/translations/sk.json index 5ada995aa6e..666f6e28840 100644 --- a/homeassistant/components/kmtronic/translations/sk.json +++ b/homeassistant/components/kmtronic/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index fa014335df9..5d2ab031323 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -51,6 +51,9 @@ from .const import ( CONF_KNX_RATE_LIMIT, CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_ROUTING_BACKBONE_KEY, + CONF_KNX_ROUTING_SECURE, + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE, CONF_KNX_SECURE_DEVICE_AUTHENTICATION, CONF_KNX_SECURE_USER_ID, CONF_KNX_SECURE_USER_PASSWORD, @@ -80,6 +83,7 @@ from .schema import ( SelectSchema, SensorSchema, SwitchSchema, + TextSchema, WeatherSchema, ga_validator, sensor_type_validator, @@ -130,6 +134,7 @@ CONFIG_SCHEMA = vol.Schema( **SelectSchema.platform_node(), **SensorSchema.platform_node(), **SwitchSchema.platform_node(), + **TextSchema.platform_node(), **WeatherSchema.platform_node(), } ), @@ -362,11 +367,8 @@ class KNXModule: def init_xknx(self) -> None: """Initialize XKNX object.""" self.xknx = XKNX( - own_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS], - rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT], - multicast_group=self.entry.data[CONF_KNX_MCAST_GRP], - multicast_port=self.entry.data[CONF_KNX_MCAST_PORT], connection_config=self.connection_config(), + rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT], state_updater=self.entry.data[CONF_KNX_STATE_UPDATER], ) @@ -384,6 +386,9 @@ class KNXModule: if _conn_type == CONF_KNX_ROUTING: return ConnectionConfig( connection_type=ConnectionType.ROUTING, + individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS], + multicast_group=self.entry.data[CONF_KNX_MCAST_GRP], + multicast_port=self.entry.data[CONF_KNX_MCAST_PORT], local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP), auto_reconnect=True, threaded=True, @@ -406,15 +411,15 @@ class KNXModule: auto_reconnect=True, threaded=True, ) - if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE: - knxkeys_file: str | None = ( - self.hass.config.path( - STORAGE_DIR, - self.entry.data[CONF_KNX_KNXKEY_FILENAME], - ) - if self.entry.data.get(CONF_KNX_KNXKEY_FILENAME) is not None - else None + knxkeys_file: str | None = ( + self.hass.config.path( + STORAGE_DIR, + self.entry.data[CONF_KNX_KNXKEY_FILENAME], ) + if self.entry.data.get(CONF_KNX_KNXKEY_FILENAME) is not None + else None + ) + if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE: return ConnectionConfig( connection_type=ConnectionType.TUNNELING_TCP_SECURE, gateway_ip=self.entry.data[CONF_HOST], @@ -431,6 +436,24 @@ class KNXModule: auto_reconnect=True, threaded=True, ) + if _conn_type == CONF_KNX_ROUTING_SECURE: + return ConnectionConfig( + connection_type=ConnectionType.ROUTING_SECURE, + individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS], + multicast_group=self.entry.data[CONF_KNX_MCAST_GRP], + multicast_port=self.entry.data[CONF_KNX_MCAST_PORT], + local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP), + secure_config=SecureConfig( + backbone_key=self.entry.data.get(CONF_KNX_ROUTING_BACKBONE_KEY), + latency_ms=self.entry.data.get( + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE + ), + knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD), + knxkeys_file_path=knxkeys_file, + ), + auto_reconnect=True, + threaded=True, + ) return ConnectionConfig( auto_reconnect=True, threaded=True, @@ -500,7 +523,7 @@ class KNXModule: transcoder := DPTBase.parse_transcoder(dpt) ): self._address_filter_transcoder.update( - {_filter: transcoder for _filter in _filters} # type: ignore[misc] + {_filter: transcoder for _filter in _filters} # type: ignore[type-abstract] ) return self.xknx.telegram_queue.register_telegram_received_cb( @@ -532,7 +555,7 @@ class KNXModule: transcoder := DPTBase.parse_transcoder(dpt) ): self._group_address_transcoder.update( - {_address: transcoder for _address in group_addresses} # type: ignore[misc] + {_address: transcoder for _address in group_addresses} # type: ignore[type-abstract] ) for group_address in group_addresses: if group_address in self._knx_event_callback.group_addresses: diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index d6516d1d4ef..f7ef0d943a9 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -1,22 +1,25 @@ """Config flow for KNX.""" from __future__ import annotations +from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator from typing import Any, Final import voluptuous as vol from xknx import XKNX -from xknx.exceptions.exception import InvalidSignature +from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner -from xknx.secure import load_key_ring +from xknx.io.self_description import request_description +from xknx.secure import load_keyring -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowHandler, FlowResult from homeassistant.helpers import selector from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.helpers.typing import UNDEFINED from .const import ( CONF_KNX_AUTOMATIC, @@ -32,6 +35,9 @@ from .const import ( CONF_KNX_RATE_LIMIT, CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_ROUTING_BACKBONE_KEY, + CONF_KNX_ROUTING_SECURE, + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE, CONF_KNX_SECURE_DEVICE_AUTHENTICATION, CONF_KNX_SECURE_USER_ID, CONF_KNX_SECURE_USER_PASSWORD, @@ -40,6 +46,7 @@ from .const import ( CONF_KNX_TUNNELING_TCP, CONF_KNX_TUNNELING_TCP_SECURE, CONST_KNX_STORAGE_KEY, + DEFAULT_ROUTING_IA, DOMAIN, KNXConfigEntryData, ) @@ -47,21 +54,25 @@ from .schema import ia_validator, ip_v4_validator CONF_KNX_GATEWAY: Final = "gateway" CONF_MAX_RATE_LIMIT: Final = 60 -CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0" DEFAULT_ENTRY_DATA = KNXConfigEntryData( - individual_address=XKNX.DEFAULT_ADDRESS, + individual_address=DEFAULT_ROUTING_IA, + local_ip=None, multicast_group=DEFAULT_MCAST_GRP, multicast_port=DEFAULT_MCAST_PORT, - state_updater=CONF_KNX_DEFAULT_STATE_UPDATER, rate_limit=CONF_KNX_DEFAULT_RATE_LIMIT, + route_back=False, + state_updater=CONF_KNX_DEFAULT_STATE_UPDATER, ) CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type" -CONF_KNX_LABEL_TUNNELING_TCP: Final = "TCP" -CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure" -CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP" -CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode" +CONF_KNX_TUNNELING_TYPE_LABELS: Final = { + CONF_KNX_TUNNELING: "UDP (Tunnelling v1)", + CONF_KNX_TUNNELING_TCP: "TCP (Tunnelling v2)", + CONF_KNX_TUNNELING_TCP_SECURE: "Secure Tunnelling (TCP)", +} + +OPTION_MANUAL_TUNNEL: Final = "Manual" _IA_SELECTOR = selector.TextSelector() _IP_SELECTOR = selector.TextSelector() @@ -75,88 +86,131 @@ _PORT_SELECTOR = vol.All( ) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a KNX config flow.""" +class KNXCommonFlow(ABC, FlowHandler): + """Base class for KNX flows.""" - VERSION = 1 + def __init__(self, initial_data: KNXConfigEntryData) -> None: + """Initialize KNXCommonFlow.""" + self.initial_data = initial_data + self.new_entry_data = KNXConfigEntryData() + self._found_gateways: list[GatewayDescriptor] = [] + self._found_tunnels: list[GatewayDescriptor] = [] + self._selected_tunnel: GatewayDescriptor | None = None - _found_tunnels: list[GatewayDescriptor] - _selected_tunnel: GatewayDescriptor | None - _tunneling_config: KNXConfigEntryData | None + self._gatewayscanner: GatewayScanner | None = None + self._async_scan_gen: AsyncGenerator[GatewayDescriptor, None] | None = None - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> KNXOptionsFlowHandler: - """Get the options flow for this handler.""" - return KNXOptionsFlowHandler(config_entry) + @abstractmethod + def finish_flow(self, title: str) -> FlowResult: + """Finish the flow.""" - async def async_step_user(self, user_input: dict | None = None) -> FlowResult: - """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - self._found_tunnels = [] - self._selected_tunnel = None - self._tunneling_config = None - return await self.async_step_type() - - async def async_step_type(self, user_input: dict | None = None) -> FlowResult: + async def async_step_connection_type( + self, user_input: dict | None = None + ) -> FlowResult: """Handle connection type configuration.""" if user_input is not None: + if self._async_scan_gen: + await self._async_scan_gen.aclose() # stop the scan + self._async_scan_gen = None + if self._gatewayscanner: + self._found_gateways = list( + self._gatewayscanner.found_gateways.values() + ) connection_type = user_input[CONF_KNX_CONNECTION_TYPE] - if connection_type == CONF_KNX_AUTOMATIC: - entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData( - connection_type=CONF_KNX_AUTOMATIC - ) - return self.async_create_entry( - title=CONF_KNX_AUTOMATIC.capitalize(), - data=entry_data, - ) - if connection_type == CONF_KNX_ROUTING: return await self.async_step_routing() - if connection_type == CONF_KNX_TUNNELING and self._found_tunnels: + if connection_type == CONF_KNX_TUNNELING: + self._found_tunnels = [ + gateway + for gateway in self._found_gateways + if gateway.supports_tunnelling + ] + self._found_tunnels.sort( + key=lambda tunnel: tunnel.individual_address.raw + if tunnel.individual_address + else 0 + ) return await self.async_step_tunnel() - return await self.async_step_manual_tunnel() + # Automatic connection type + self.new_entry_data = KNXConfigEntryData(connection_type=CONF_KNX_AUTOMATIC) + return self.finish_flow(title=CONF_KNX_AUTOMATIC.capitalize()) supported_connection_types = { CONF_KNX_TUNNELING: CONF_KNX_TUNNELING.capitalize(), CONF_KNX_ROUTING: CONF_KNX_ROUTING.capitalize(), } - if gateways := await scan_for_gateways(): + + if isinstance(self, OptionsFlow) and (knx_module := self.hass.data.get(DOMAIN)): + xknx = knx_module.xknx + else: + xknx = XKNX() + self._gatewayscanner = GatewayScanner( + xknx, stop_on_found=0, timeout_in_seconds=2 + ) + # keep a reference to the generator to scan in background until user selects a connection type + self._async_scan_gen = self._gatewayscanner.async_scan() + try: + await self._async_scan_gen.__anext__() + except StopAsyncIteration: + pass # scan finished, no interfaces discovered + else: # add automatic at first position only if a gateway responded supported_connection_types = { CONF_KNX_AUTOMATIC: CONF_KNX_AUTOMATIC.capitalize() } | supported_connection_types - self._found_tunnels = [ - gateway for gateway in gateways if gateway.supports_tunnelling - ] fields = { vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types) } - return self.async_show_form(step_id="type", data_schema=vol.Schema(fields)) + return self.async_show_form( + step_id="connection_type", data_schema=vol.Schema(fields) + ) async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult: """Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found.""" if user_input is not None: + if user_input[CONF_KNX_GATEWAY] == OPTION_MANUAL_TUNNEL: + if self._found_tunnels: + self._selected_tunnel = self._found_tunnels[0] + return await self.async_step_manual_tunnel() + self._selected_tunnel = next( tunnel for tunnel in self._found_tunnels if user_input[CONF_KNX_GATEWAY] == str(tunnel) ) - return await self.async_step_manual_tunnel() + connection_type = ( + CONF_KNX_TUNNELING_TCP_SECURE + if self._selected_tunnel.tunnelling_requires_secure + else CONF_KNX_TUNNELING_TCP + if self._selected_tunnel.supports_tunnelling_tcp + else CONF_KNX_TUNNELING + ) + self.new_entry_data = KNXConfigEntryData( + host=self._selected_tunnel.ip_addr, + port=self._selected_tunnel.port, + route_back=False, + connection_type=connection_type, + ) + if connection_type == CONF_KNX_TUNNELING_TCP_SECURE: + return self.async_show_menu( + step_id="secure_key_source", + menu_options=["secure_knxkeys", "secure_tunnel_manual"], + ) + return self.finish_flow(title=f"Tunneling @ {self._selected_tunnel}") - # skip this step if the user has only one unique gateway. - if len(self._found_tunnels) == 1: - self._selected_tunnel = self._found_tunnels[0] + if not self._found_tunnels: return await self.async_step_manual_tunnel() errors: dict = {} - tunnels_repr = {str(tunnel) for tunnel in self._found_tunnels} - fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnels_repr)} + tunnel_options = { + str(tunnel): f"{tunnel}{' 🔐' if tunnel.tunnelling_requires_secure else ''}" + for tunnel in self._found_tunnels + } + tunnel_options |= {OPTION_MANUAL_TUNNEL: OPTION_MANUAL_TUNNEL} + fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnel_options)} return self.async_show_form( step_id="tunnel", data_schema=vol.Schema(fields), errors=errors @@ -180,84 +234,123 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except vol.Invalid: errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address" + selected_tunnelling_type = user_input[CONF_KNX_TUNNELING_TYPE] if not errors: - connection_type = user_input[CONF_KNX_TUNNELING_TYPE] - entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData( + try: + self._selected_tunnel = await request_description( + gateway_ip=_host, + gateway_port=user_input[CONF_PORT], + local_ip=_local_ip, + route_back=user_input[CONF_KNX_ROUTE_BACK], + ) + except CommunicationError: + errors["base"] = "cannot_connect" + else: + if bool(self._selected_tunnel.tunnelling_requires_secure) is not ( + selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE + ): + errors[CONF_KNX_TUNNELING_TYPE] = "unsupported_tunnel_type" + elif ( + selected_tunnelling_type == CONF_KNX_TUNNELING_TCP + and not self._selected_tunnel.supports_tunnelling_tcp + ): + errors[CONF_KNX_TUNNELING_TYPE] = "unsupported_tunnel_type" + + if not errors: + self.new_entry_data = KNXConfigEntryData( + connection_type=selected_tunnelling_type, host=_host, port=user_input[CONF_PORT], - route_back=( - connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK - ), + route_back=user_input[CONF_KNX_ROUTE_BACK], local_ip=_local_ip, - connection_type=( - CONF_KNX_TUNNELING_TCP - if connection_type == CONF_KNX_LABEL_TUNNELING_TCP - else CONF_KNX_TUNNELING - ), ) - if connection_type == CONF_KNX_LABEL_TUNNELING_TCP_SECURE: - self._tunneling_config = entry_data + if selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE: return self.async_show_menu( - step_id="secure_tunneling", - menu_options=["secure_knxkeys", "secure_manual"], + step_id="secure_key_source", + menu_options=["secure_knxkeys", "secure_routing_manual"], ) + return self.finish_flow(title=f"Tunneling @ {_host}") - return self.async_create_entry( - title=f"Tunneling @ {_host}", - data=entry_data, - ) - - connection_methods: list[str] = [ - CONF_KNX_LABEL_TUNNELING_TCP, - CONF_KNX_LABEL_TUNNELING_UDP, - CONF_KNX_LABEL_TUNNELING_TCP_SECURE, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - ] - ip_address = "" - port = DEFAULT_MCAST_PORT - if self._selected_tunnel is not None: + _reconfiguring_existing_tunnel = ( + self.initial_data.get(CONF_KNX_CONNECTION_TYPE) + in CONF_KNX_TUNNELING_TYPE_LABELS + ) + if ( # initial attempt on ConfigFlow or coming from automatic / routing + (isinstance(self, ConfigFlow) or not _reconfiguring_existing_tunnel) + and not user_input + and self._selected_tunnel is not None + ): # default to first found tunnel ip_address = self._selected_tunnel.ip_addr port = self._selected_tunnel.port - if not self._selected_tunnel.supports_tunnelling_tcp: - connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP) - connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP_SECURE) + if self._selected_tunnel.tunnelling_requires_secure: + default_type = CONF_KNX_TUNNELING_TCP_SECURE + elif self._selected_tunnel.supports_tunnelling_tcp: + default_type = CONF_KNX_TUNNELING_TCP + else: + default_type = CONF_KNX_TUNNELING + else: # OptionFlow, no tunnel discovered or user input + ip_address = ( + user_input[CONF_HOST] + if user_input + else self.initial_data.get(CONF_HOST) + ) + port = ( + user_input[CONF_PORT] + if user_input + else self.initial_data.get(CONF_PORT, DEFAULT_MCAST_PORT) + ) + default_type = ( + user_input[CONF_KNX_TUNNELING_TYPE] + if user_input + else self.initial_data[CONF_KNX_CONNECTION_TYPE] + if _reconfiguring_existing_tunnel + else CONF_KNX_TUNNELING + ) + _route_back: bool = self.initial_data.get( + CONF_KNX_ROUTE_BACK, not bool(self._selected_tunnel) + ) fields = { - vol.Required(CONF_KNX_TUNNELING_TYPE): vol.In(connection_methods), + vol.Required(CONF_KNX_TUNNELING_TYPE, default=default_type): vol.In( + CONF_KNX_TUNNELING_TYPE_LABELS + ), vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR, vol.Required(CONF_PORT, default=port): _PORT_SELECTOR, + vol.Required( + CONF_KNX_ROUTE_BACK, default=_route_back + ): selector.BooleanSelector(), } - if self.show_advanced_options: fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR + if not self._found_tunnels and not errors.get("base"): + errors["base"] = "no_tunnel_discovered" return self.async_show_form( step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_secure_manual( + async def async_step_secure_tunnel_manual( self, user_input: dict | None = None ) -> FlowResult: - """Configure ip secure manually.""" + """Configure ip secure tunnelling manually.""" errors: dict = {} if user_input is not None: - assert self._tunneling_config - entry_data = self._tunneling_config | KNXConfigEntryData( - connection_type=CONF_KNX_TUNNELING_TCP_SECURE, + self.new_entry_data |= KNXConfigEntryData( device_authentication=user_input[CONF_KNX_SECURE_DEVICE_AUTHENTICATION], user_id=user_input[CONF_KNX_SECURE_USER_ID], user_password=user_input[CONF_KNX_SECURE_USER_PASSWORD], ) - - return self.async_create_entry( - title=f"Secure Tunneling @ {self._tunneling_config[CONF_HOST]}", - data=entry_data, + return self.finish_flow( + title=f"Secure Tunneling @ {self.new_entry_data[CONF_HOST]}" ) fields = { - vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All( + vol.Required( + CONF_KNX_SECURE_USER_ID, + default=self.initial_data.get(CONF_KNX_SECURE_USER_ID, 2), + ): vol.All( selector.NumberSelector( selector.NumberSelectorConfig( min=1, max=127, mode=selector.NumberSelectorMode.BOX @@ -265,16 +358,78 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), vol.Coerce(int), ), - vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.TextSelector( + vol.Required( + CONF_KNX_SECURE_USER_PASSWORD, + default=self.initial_data.get(CONF_KNX_SECURE_USER_PASSWORD), + ): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD), ), - vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.TextSelector( + vol.Required( + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + default=self.initial_data.get(CONF_KNX_SECURE_DEVICE_AUTHENTICATION), + ): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD), ), } return self.async_show_form( - step_id="secure_manual", data_schema=vol.Schema(fields), errors=errors + step_id="secure_tunnel_manual", + data_schema=vol.Schema(fields), + errors=errors, + ) + + async def async_step_secure_routing_manual( + self, user_input: dict | None = None + ) -> FlowResult: + """Configure ip secure routing manually.""" + errors: dict = {} + + if user_input is not None: + try: + key_bytes = bytes.fromhex(user_input[CONF_KNX_ROUTING_BACKBONE_KEY]) + if len(key_bytes) != 16: + raise ValueError + except ValueError: + errors[CONF_KNX_ROUTING_BACKBONE_KEY] = "invalid_backbone_key" + if not errors: + self.new_entry_data |= KNXConfigEntryData( + backbone_key=user_input[CONF_KNX_ROUTING_BACKBONE_KEY], + sync_latency_tolerance=user_input[ + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE + ], + ) + return self.finish_flow( + title=f"Secure Routing as {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}" + ) + + fields = { + vol.Required( + CONF_KNX_ROUTING_BACKBONE_KEY, + default=self.initial_data.get(CONF_KNX_ROUTING_BACKBONE_KEY), + ): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD), + ), + vol.Required( + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE, + default=self.initial_data.get(CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE) + or 1000, + ): vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=400, + max=4000, + unit_of_measurement="ms", + mode=selector.NumberSelectorMode.BOX, + ), + ), + vol.Coerce(int), + ), + } + + return self.async_show_form( + step_id="secure_routing_manual", + data_schema=vol.Schema(fields), + errors=errors, ) async def async_step_secure_knxkeys( @@ -284,33 +439,46 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - assert self._tunneling_config storage_key = CONST_KNX_STORAGE_KEY + user_input[CONF_KNX_KNXKEY_FILENAME] try: - load_key_ring( + await load_keyring( path=self.hass.config.path(STORAGE_DIR, storage_key), password=user_input[CONF_KNX_KNXKEY_PASSWORD], ) except FileNotFoundError: errors[CONF_KNX_KNXKEY_FILENAME] = "file_not_found" - except InvalidSignature: + except InvalidSecureConfiguration: errors[CONF_KNX_KNXKEY_PASSWORD] = "invalid_signature" if not errors: - entry_data = self._tunneling_config | KNXConfigEntryData( - connection_type=CONF_KNX_TUNNELING_TCP_SECURE, + self.new_entry_data |= KNXConfigEntryData( knxkeys_filename=storage_key, knxkeys_password=user_input[CONF_KNX_KNXKEY_PASSWORD], + backbone_key=None, + sync_latency_tolerance=None, + device_authentication=None, + user_id=None, + user_password=None, ) + if ( + self.new_entry_data[CONF_KNX_CONNECTION_TYPE] + == CONF_KNX_ROUTING_SECURE + ): + title = f"Secure Routing as {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}" + else: + title = f"Secure Tunneling @ {self.new_entry_data[CONF_HOST]}" + return self.finish_flow(title=title) - return self.async_create_entry( - title=f"Secure Tunneling @ {self._tunneling_config[CONF_HOST]}", - data=entry_data, - ) - + if _default_filename := self.initial_data.get(CONF_KNX_KNXKEY_FILENAME): + _default_filename = _default_filename.lstrip(CONST_KNX_STORAGE_KEY) fields = { - vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.TextSelector(), - vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.TextSelector(), + vol.Required( + CONF_KNX_KNXKEY_FILENAME, default=_default_filename + ): selector.TextSelector(), + vol.Required( + CONF_KNX_KNXKEY_PASSWORD, + default=self.initial_data.get(CONF_KNX_KNXKEY_PASSWORD), + ): selector.TextSelector(), } return self.async_show_form( @@ -323,10 +491,17 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _individual_address = ( user_input[CONF_KNX_INDIVIDUAL_ADDRESS] if user_input - else XKNX.DEFAULT_ADDRESS + else self.initial_data[CONF_KNX_INDIVIDUAL_ADDRESS] ) _multicast_group = ( - user_input[CONF_KNX_MCAST_GRP] if user_input else DEFAULT_MCAST_GRP + user_input[CONF_KNX_MCAST_GRP] + if user_input + else self.initial_data[CONF_KNX_MCAST_GRP] + ) + _multicast_port = ( + user_input[CONF_KNX_MCAST_PORT] + if user_input + else self.initial_data[CONF_KNX_MCAST_PORT] ) if user_input is not None: @@ -345,27 +520,42 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address" if not errors: - entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData( - connection_type=CONF_KNX_ROUTING, + connection_type = ( + CONF_KNX_ROUTING_SECURE + if user_input[CONF_KNX_ROUTING_SECURE] + else CONF_KNX_ROUTING + ) + self.new_entry_data = KNXConfigEntryData( + connection_type=connection_type, individual_address=_individual_address, multicast_group=_multicast_group, - multicast_port=user_input[CONF_KNX_MCAST_PORT], + multicast_port=_multicast_port, local_ip=_local_ip, ) - return self.async_create_entry( - title=CONF_KNX_ROUTING.capitalize(), data=entry_data - ) + if connection_type == CONF_KNX_ROUTING_SECURE: + return self.async_show_menu( + step_id="secure_key_source", + menu_options=["secure_knxkeys", "secure_routing_manual"], + ) + return self.finish_flow(title=f"Routing as {_individual_address}") + + routers = [router for router in self._found_gateways if router.supports_routing] + if not routers: + errors["base"] = "no_router_discovered" + default_secure_routing_enable = any( + router for router in routers if router.routing_requires_secure + ) fields = { vol.Required( CONF_KNX_INDIVIDUAL_ADDRESS, default=_individual_address ): _IA_SELECTOR, - vol.Required(CONF_KNX_MCAST_GRP, default=_multicast_group): _IP_SELECTOR, vol.Required( - CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT - ): _PORT_SELECTOR, + CONF_KNX_ROUTING_SECURE, default=default_secure_routing_enable + ): selector.BooleanSelector(), + vol.Required(CONF_KNX_MCAST_GRP, default=_multicast_group): _IP_SELECTOR, + vol.Required(CONF_KNX_MCAST_PORT, default=_multicast_port): _PORT_SELECTOR, } - if self.show_advanced_options: # Optional with default doesn't work properly in flow UI fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR @@ -375,87 +565,92 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class KNXOptionsFlowHandler(OptionsFlow): +class KNXConfigFlow(KNXCommonFlow, ConfigFlow, domain=DOMAIN): + """Handle a KNX config flow.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize KNX options flow.""" + super().__init__(initial_data=DEFAULT_ENTRY_DATA) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> KNXOptionsFlow: + """Get the options flow for this handler.""" + return KNXOptionsFlow(config_entry) + + @callback + def finish_flow(self, title: str) -> FlowResult: + """Create the ConfigEntry.""" + return self.async_create_entry( + title=title, + data=DEFAULT_ENTRY_DATA | self.new_entry_data, + ) + + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + return await self.async_step_connection_type() + + +class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): """Handle KNX options.""" general_settings: dict - current_config: dict def __init__(self, config_entry: ConfigEntry) -> None: """Initialize KNX options flow.""" self.config_entry = config_entry + super().__init__(initial_data=config_entry.data) # type: ignore[arg-type] + + @callback + def finish_flow(self, title: str | None) -> FlowResult: + """Update the ConfigEntry and finish the flow.""" + new_data = DEFAULT_ENTRY_DATA | self.initial_data | self.new_entry_data + self.hass.config_entries.async_update_entry( + self.config_entry, + data=new_data, + title=title or UNDEFINED, + ) + return self.async_create_entry(title="", data={}) async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage KNX options.""" - if user_input is not None: - self.general_settings = user_input - return await self.async_step_tunnel() + return self.async_show_menu( + step_id="options_init", + menu_options=["connection_type", "communication_settings"], + ) - supported_connection_types = [ - CONF_KNX_AUTOMATIC, - CONF_KNX_TUNNELING, - CONF_KNX_ROUTING, - ] - self.current_config = self.config_entry.data # type: ignore[assignment] + async def async_step_communication_settings( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage KNX communication settings.""" + if user_input is not None: + self.new_entry_data = KNXConfigEntryData( + state_updater=user_input[CONF_KNX_STATE_UPDATER], + rate_limit=user_input[CONF_KNX_RATE_LIMIT], + ) + return self.finish_flow(title=None) data_schema = { vol.Required( - CONF_KNX_CONNECTION_TYPE, - default=( - CONF_KNX_TUNNELING - if self.current_config.get(CONF_KNX_CONNECTION_TYPE) - == CONF_KNX_TUNNELING_TCP - else self.current_config.get(CONF_KNX_CONNECTION_TYPE) - ), - ): vol.In(supported_connection_types), - vol.Required( - CONF_KNX_INDIVIDUAL_ADDRESS, - default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS], - ): selector.TextSelector(), - vol.Required( - CONF_KNX_MCAST_GRP, - default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP), - ): _IP_SELECTOR, - vol.Required( - CONF_KNX_MCAST_PORT, - default=self.current_config.get( - CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT - ), - ): _PORT_SELECTOR, - } - - if self.show_advanced_options: - local_ip = ( - self.current_config.get(CONF_KNX_LOCAL_IP) - if self.current_config.get(CONF_KNX_LOCAL_IP) is not None - else CONF_DEFAULT_LOCAL_IP - ) - data_schema[ - vol.Required( - CONF_KNX_LOCAL_IP, - default=local_ip, - ) - ] = _IP_SELECTOR - data_schema[ - vol.Required( + CONF_KNX_STATE_UPDATER, + default=self.initial_data.get( CONF_KNX_STATE_UPDATER, - default=self.current_config.get( - CONF_KNX_STATE_UPDATER, - CONF_KNX_DEFAULT_STATE_UPDATER, - ), - ) - ] = selector.BooleanSelector() - data_schema[ - vol.Required( + CONF_KNX_DEFAULT_STATE_UPDATER, + ), + ): selector.BooleanSelector(), + vol.Required( + CONF_KNX_RATE_LIMIT, + default=self.initial_data.get( CONF_KNX_RATE_LIMIT, - default=self.current_config.get( - CONF_KNX_RATE_LIMIT, - CONF_KNX_DEFAULT_RATE_LIMIT, - ), - ) - ] = vol.All( + CONF_KNX_DEFAULT_RATE_LIMIT, + ), + ): vol.All( selector.NumberSelector( selector.NumberSelectorConfig( min=0, @@ -464,101 +659,10 @@ class KNXOptionsFlowHandler(OptionsFlow): ), ), vol.Coerce(int), - ) - + ), + } return self.async_show_form( - step_id="init", + step_id="communication_settings", data_schema=vol.Schema(data_schema), - last_step=self.current_config.get(CONF_KNX_CONNECTION_TYPE) - != CONF_KNX_TUNNELING, + last_step=True, ) - - async def async_step_tunnel( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage KNX tunneling options.""" - if ( - self.general_settings.get(CONF_KNX_CONNECTION_TYPE) == CONF_KNX_TUNNELING - and user_input is None - ): - connection_methods: list[str] = [ - CONF_KNX_LABEL_TUNNELING_TCP, - CONF_KNX_LABEL_TUNNELING_UDP, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - ] - return self.async_show_form( - step_id="tunnel", - data_schema=vol.Schema( - { - vol.Required( - CONF_KNX_TUNNELING_TYPE, - default=get_knx_tunneling_type(self.current_config), - ): vol.In(connection_methods), - vol.Required( - CONF_HOST, default=self.current_config.get(CONF_HOST) - ): _IP_SELECTOR, - vol.Required( - CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) - ): _PORT_SELECTOR, - } - ), - last_step=True, - ) - - _local_ip = self.general_settings.get(CONF_KNX_LOCAL_IP) - entry_data = ( - DEFAULT_ENTRY_DATA - | self.general_settings - | KNXConfigEntryData( - host=self.current_config.get(CONF_HOST, ""), - local_ip=_local_ip if _local_ip != CONF_DEFAULT_LOCAL_IP else None, - ) - ) - - if user_input is not None: - connection_type = user_input[CONF_KNX_TUNNELING_TYPE] - entry_data = entry_data | KNXConfigEntryData( - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - route_back=(connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK), - connection_type=( - CONF_KNX_TUNNELING_TCP - if connection_type == CONF_KNX_LABEL_TUNNELING_TCP - else CONF_KNX_TUNNELING - ), - ) - - entry_title = str(entry_data[CONF_KNX_CONNECTION_TYPE]).capitalize() - if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING: - entry_title = f"Tunneling @ {entry_data[CONF_HOST]}" - if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING_TCP: - entry_title = f"Tunneling @ {entry_data[CONF_HOST]} (TCP)" - - self.hass.config_entries.async_update_entry( - self.config_entry, - data=entry_data, - title=entry_title, - ) - - return self.async_create_entry(title="", data={}) - - -def get_knx_tunneling_type(config_entry_data: dict) -> str: - """Obtain the knx tunneling type based on the data in the config entry data.""" - connection_type = config_entry_data[CONF_KNX_CONNECTION_TYPE] - route_back = config_entry_data.get(CONF_KNX_ROUTE_BACK, False) - if route_back and connection_type == CONF_KNX_TUNNELING: - return CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK - if connection_type == CONF_KNX_TUNNELING_TCP: - return CONF_KNX_LABEL_TUNNELING_TCP - - return CONF_KNX_LABEL_TUNNELING_UDP - - -async def scan_for_gateways(stop_on_found: int = 0) -> list[GatewayDescriptor]: - """Scan for gateways within the network.""" - xknx = XKNX() - gatewayscanner = GatewayScanner( - xknx, stop_on_found=stop_on_found, timeout_in_seconds=2 - ) - return await gatewayscanner.scan() diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 3cc73a6dd35..058223bfaa1 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -30,6 +30,9 @@ CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" CONF_KNX_CONNECTION_TYPE: Final = "connection_type" CONF_KNX_AUTOMATIC: Final = "automatic" CONF_KNX_ROUTING: Final = "routing" +CONF_KNX_ROUTING_BACKBONE_KEY: Final = "backbone_key" +CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: Final = "sync_latency_tolerance" +CONF_KNX_ROUTING_SECURE: Final = "routing_secure" CONF_KNX_TUNNELING: Final = "tunneling" CONF_KNX_TUNNELING_TCP: Final = "tunneling_tcp" CONF_KNX_TUNNELING_TCP_SECURE: Final = "tunneling_tcp_secure" @@ -41,7 +44,9 @@ CONF_KNX_RATE_LIMIT: Final = "rate_limit" CONF_KNX_ROUTE_BACK: Final = "route_back" CONF_KNX_STATE_UPDATER: Final = "state_updater" CONF_KNX_DEFAULT_STATE_UPDATER: Final = True -CONF_KNX_DEFAULT_RATE_LIMIT: Final = 20 +CONF_KNX_DEFAULT_RATE_LIMIT: Final = 0 + +DEFAULT_ROUTING_IA: Final = "0.0.240" ## # Secure constants @@ -85,11 +90,13 @@ class KNXConfigEntryData(TypedDict, total=False): host: str port: int - user_id: int - user_password: str - device_authentication: str + user_id: int | None + user_password: str | None + device_authentication: str | None knxkeys_filename: str knxkeys_password: str + backbone_key: str | None + sync_latency_tolerance: int | None class ColorTempModes(Enum): @@ -112,6 +119,7 @@ SUPPORTED_PLATFORMS: Final = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.TEXT, Platform.WEATHER, ] diff --git a/homeassistant/components/knx/diagnostics.py b/homeassistant/components/knx/diagnostics.py index c409b4116bf..3dd14aef653 100644 --- a/homeassistant/components/knx/diagnostics.py +++ b/homeassistant/components/knx/diagnostics.py @@ -13,12 +13,14 @@ from homeassistant.core import HomeAssistant from . import CONFIG_SCHEMA from .const import ( CONF_KNX_KNXKEY_PASSWORD, + CONF_KNX_ROUTING_BACKBONE_KEY, CONF_KNX_SECURE_DEVICE_AUTHENTICATION, CONF_KNX_SECURE_USER_PASSWORD, DOMAIN, ) TO_REDACT = { + CONF_KNX_ROUTING_BACKBONE_KEY, CONF_KNX_KNXKEY_PASSWORD, CONF_KNX_SECURE_USER_PASSWORD, CONF_KNX_SECURE_DEVICE_AUTHENTICATION, diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 27f9fb963f7..60db7e95a65 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -76,9 +76,9 @@ class KNXFan(KnxEntity, FanEntity): await self._device.set_speed(percentage) @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - flags: int = FanEntityFeature.SET_SPEED + flags = FanEntityFeature.SET_SPEED if self._device.supports_oscillation: flags |= FanEntityFeature.OSCILLATE diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index a7436ef1ae3..4bf1b13672b 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.2.1"], + "requirements": ["xknx==2.1.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index c1615b7e8e2..87a7b6fdab5 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -21,10 +21,15 @@ from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) from homeassistant.components.number import NumberMode -from homeassistant.components.sensor import CONF_STATE_CLASS, STATE_CLASSES_SCHEMA +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, + STATE_CLASSES_SCHEMA, +) from homeassistant.components.switch import ( DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA, ) +from homeassistant.components.text import TextMode from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, @@ -75,8 +80,8 @@ def dpt_subclass_validator(dpt_base_class: type[DPTBase]) -> Callable[[Any], str return dpt_value_validator -numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[misc] -sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[misc] +numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[type-abstract] +sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract] string_type_validator = dpt_subclass_validator(DPTString) @@ -130,7 +135,7 @@ def number_limit_sub_validator(entity_config: OrderedDict) -> OrderedDict: if dpt_class is None: raise vol.Invalid(f"'type: {value_type}' is not a valid numeric sensor type.") - # Inifinity is not supported by Home Assistant frontend so user defined + # Infinity is not supported by Home Assistant frontend so user defined # config is required if if xknx DPTNumeric subclass defines it as limit. if min_config is None and dpt_class.value_min == float("-inf"): raise vol.Invalid(f"'min' key required for value type '{value_type}'") @@ -855,6 +860,7 @@ class SensorSchema(KNXPlatformSchema): vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Required(CONF_TYPE): sensor_type_validator, vol.Required(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -882,6 +888,26 @@ class SwitchSchema(KNXPlatformSchema): ) +class TextSchema(KNXPlatformSchema): + """Voluptuous schema for KNX text.""" + + PLATFORM = Platform.TEXT + + DEFAULT_NAME = "KNX Text" + + ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, + vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator, + vol.Optional(CONF_MODE, default=TextMode.TEXT): vol.Coerce(TextMode), + vol.Required(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + } + ) + + class WeatherSchema(KNXPlatformSchema): """Voluptuous schema for KNX weather station.""" diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index ceb9f435d83..395b17e44a6 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,6 +1,7 @@ """Support for KNX/IP sensors.""" from __future__ import annotations +from contextlib import suppress from typing import Any from xknx import XKNX @@ -9,10 +10,16 @@ from xknx.devices import Sensor as XknxSensor from homeassistant import config_entries from homeassistant.components.sensor import ( CONF_STATE_CLASS, - DEVICE_CLASSES, + SensorDeviceClass, SensorEntity, ) -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE, Platform +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_NAME, + CONF_TYPE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, StateType @@ -54,11 +61,13 @@ class KNXSensor(KnxEntity, SensorEntity): def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" super().__init__(_create_sensor(xknx, config)) - self._attr_device_class = ( - self._device.ha_device_class() - if self._device.ha_device_class() in DEVICE_CLASSES - else None - ) + if device_class := config.get(CONF_DEVICE_CLASS): + self._attr_device_class = device_class + else: + with suppress(ValueError): + self._attr_device_class = SensorDeviceClass( + str(self._device.ha_device_class()) + ) self._attr_force_update = self._device.always_callback self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.sensor_value.group_address_state) diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index c8161462d66..632af9961dc 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -1,7 +1,7 @@ { "config": { "step": { - "type": { + "connection_type": { "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", "data": { "connection_type": "KNX Connection Type" @@ -19,19 +19,22 @@ "tunneling_type": "KNX Tunneling Type", "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", + "route_back": "Route back / NAT mode", "local_ip": "Local IP of Home Assistant" }, "data_description": { "port": "Port of the KNX/IP tunneling device.", "host": "IP address of the KNX/IP tunneling device.", + "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections.", "local_ip": "Leave blank to use auto-discovery." } }, - "secure_tunneling": { + "secure_key_source": { "description": "Select how you want to configure KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", - "secure_manual": "Configure IP secure keys manually" + "secure_tunnel_manual": "Configure IP secure credentials manually", + "secure_routing_manual": "Configure IP secure backbone key manually" } }, "secure_knxkeys": { @@ -45,7 +48,7 @@ "knxkeys_password": "This was set when exporting the file from ETS." } }, - "secure_manual": { + "secure_tunnel_manual": { "description": "Please enter your IP secure information.", "data": { "user_id": "User ID", @@ -58,10 +61,22 @@ "device_authentication": "This is set in the 'IP' panel of the interface in ETS." } }, + "secure_routing_manual": { + "description": "Please enter your IP secure information.", + "data": { + "backbone_key": "Backbone key", + "sync_latency_tolerance": "Network latency tolerance" + }, + "data_description": { + "backbone_key": "Can be seen in the 'Security' report of an ETS project. Eg. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Default is 1000." + } + }, "routing": { "description": "Please configure the routing options.", "data": { "individual_address": "Individual address", + "routing_secure": "Use KNX IP Secure", "multicast_group": "Multicast group", "multicast_port": "Multicast port", "local_ip": "Local IP of Home Assistant" @@ -78,44 +93,130 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", "invalid_ip_address": "Invalid IPv4 address.", "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/" + "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", + "no_router_discovered": "No KNXnet/IP router was discovered on the network.", + "no_tunnel_discovered": "Could not find a KNX tunneling server on your network.", + "unsupported_tunnel_type": "Selected tunnelling type not supported by gateway." } }, "options": { "step": { - "init": { + "options_init": { + "menu_options": { + "connection_type": "Configure KNX interface", + "communication_settings": "Communication settings" + } + }, + "communication_settings": { "data": { - "connection_type": "KNX Connection Type", - "individual_address": "Default individual address", - "multicast_group": "[%key:component::knx::config::step::routing::data::multicast_group%]", - "multicast_port": "[%key:component::knx::config::step::routing::data::multicast_port%]", - "local_ip": "Local IP of Home Assistant", "state_updater": "State updater", "rate_limit": "Rate limit" }, "data_description": { - "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", - "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", - "multicast_port": "Used for routing and discovery. Default: `3671`", - "local_ip": "Use `0.0.0.0` for auto-discovery.", "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options.", - "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40" + "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40" + } + }, + "connection_type": { + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", + "data": { + "connection_type": "KNX Connection Type" } }, "tunnel": { + "description": "[%key:component::knx::config::step::tunnel::description%]", "data": { - "tunneling_type": "KNX Tunneling Type", + "gateway": "[%key:component::knx::config::step::tunnel::data::gateway%]" + } + }, + "manual_tunnel": { + "description": "[%key:component::knx::config::step::manual_tunnel::description%]", + "data": { + "tunneling_type": "[%key:component::knx::config::step::manual_tunnel::data::tunneling_type%]", "port": "[%key:common::config_flow::data::port%]", - "host": "[%key:common::config_flow::data::host%]" + "host": "[%key:common::config_flow::data::host%]", + "route_back": "[%key:component::knx::config::step::manual_tunnel::data::route_back%]", + "local_ip": "[%key:component::knx::config::step::manual_tunnel::data::local_ip%]" }, "data_description": { "port": "[%key:component::knx::config::step::manual_tunnel::data_description::port%]", - "host": "[%key:component::knx::config::step::manual_tunnel::data_description::host%]" + "host": "[%key:component::knx::config::step::manual_tunnel::data_description::host%]", + "route_back": "[%key:component::knx::config::step::manual_tunnel::data_description::route_back%]", + "local_ip": "[%key:component::knx::config::step::manual_tunnel::data_description::local_ip%]" + } + }, + "secure_key_source": { + "description": "[%key:component::knx::config::step::secure_key_source::description%]", + "menu_options": { + "secure_knxkeys": "[%key:component::knx::config::step::secure_key_source::menu_options::secure_knxkeys%]", + "secure_tunnel_manual": "[%key:component::knx::config::step::secure_key_source::menu_options::secure_tunnel_manual%]", + "secure_routing_manual": "[%key:component::knx::config::step::secure_key_source::menu_options::secure_routing_manual%]" + } + }, + "secure_knxkeys": { + "description": "[%key:component::knx::config::step::secure_knxkeys::description%]", + "data": { + "knxkeys_filename": "[%key:component::knx::config::step::secure_knxkeys::data::knxkeys_filename%]", + "knxkeys_password": "[%key:component::knx::config::step::secure_knxkeys::data::knxkeys_password%]" + }, + "data_description": { + "knxkeys_filename": "[%key:component::knx::config::step::secure_knxkeys::data_description::knxkeys_filename%]", + "knxkeys_password": "[%key:component::knx::config::step::secure_knxkeys::data_description::knxkeys_password%]" + } + }, + "secure_tunnel_manual": { + "description": "[%key:component::knx::config::step::secure_tunnel_manual::description%]", + "data": { + "user_id": "[%key:component::knx::config::step::secure_tunnel_manual::data::user_id%]", + "user_password": "[%key:component::knx::config::step::secure_tunnel_manual::data::user_password%]", + "device_authentication": "[%key:component::knx::config::step::secure_tunnel_manual::data::device_authentication%]" + }, + "data_description": { + "user_id": "[%key:component::knx::config::step::secure_tunnel_manual::data_description::user_id%]", + "user_password": "[%key:component::knx::config::step::secure_tunnel_manual::data_description::user_password%]", + "device_authentication": "[%key:component::knx::config::step::secure_tunnel_manual::data_description::device_authentication%]" + } + }, + "secure_routing_manual": { + "description": "[%key:component::knx::config::step::secure_routing_manual::description%]", + "data": { + "backbone_key": "[%key:component::knx::config::step::secure_routing_manual::data::backbone_key%]", + "sync_latency_tolerance": "[%key:component::knx::config::step::secure_routing_manual::data::sync_latency_tolerance%]" + }, + "data_description": { + "backbone_key": "[%key:component::knx::config::step::secure_routing_manual::data_description::backbone_key%]", + "sync_latency_tolerance": "[%key:component::knx::config::step::secure_routing_manual::data_description::sync_latency_tolerance%]" + } + }, + "routing": { + "description": "[%key:component::knx::config::step::routing::description%]", + "data": { + "individual_address": "[%key:component::knx::config::step::routing::data::individual_address%]", + "routing_secure": "[%key:component::knx::config::step::routing::data::routing_secure%]", + "multicast_group": "[%key:component::knx::config::step::routing::data::multicast_group%]", + "multicast_port": "[%key:component::knx::config::step::routing::data::multicast_port%]", + "local_ip": "[%key:component::knx::config::step::routing::data::local_ip%]" + }, + "data_description": { + "individual_address": "[%key:component::knx::config::step::routing::data_description::individual_address%]", + "local_ip": "[%key:component::knx::config::step::routing::data_description::local_ip%]" } } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_backbone_key": "[%key:component::knx::config::error::invalid_backbone_key%]", + "invalid_individual_address": "[%key:component::knx::config::error::invalid_individual_address%]", + "invalid_ip_address": "[%key:component::knx::config::error::invalid_ip_address%]", + "invalid_signature": "[%key:component::knx::config::error::invalid_signature%]", + "file_not_found": "[%key:component::knx::config::error::file_not_found%]", + "no_router_discovered": "[%key:component::knx::config::error::no_router_discovered%]", + "no_tunnel_discovered": "[%key:component::knx::config::error::no_tunnel_discovered%]", + "unsupported_tunnel_type": "[%key:component::knx::config::error::unsupported_tunnel_type%]" } } } diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py new file mode 100644 index 00000000000..abd3f44ae6b --- /dev/null +++ b/homeassistant/components/knx/text.py @@ -0,0 +1,92 @@ +"""Support for KNX/IP text.""" +from __future__ import annotations + +from xknx import XKNX +from xknx.devices import Notification as XknxNotification +from xknx.dpt import DPTLatin1 + +from homeassistant import config_entries +from homeassistant.components.text import TextEntity +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_MODE, + CONF_NAME, + CONF_TYPE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_RESPOND_TO_READ, + CONF_STATE_ADDRESS, + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, +) +from .knx_entity import KnxEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensor(s) for KNX platform.""" + xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TEXT] + + async_add_entities(KNXText(xknx, entity_config) for entity_config in config) + + +def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: + """Return a KNX Notification to be used within XKNX.""" + return XknxNotification( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + group_address_state=config.get(CONF_STATE_ADDRESS), + respond_to_read=config[CONF_RESPOND_TO_READ], + value_type=config[CONF_TYPE], + ) + + +class KNXText(KnxEntity, TextEntity, RestoreEntity): + """Representation of a KNX text.""" + + _device: XknxNotification + _attr_native_max = 14 + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX text.""" + super().__init__(_create_notification(xknx, config)) + self._attr_mode = config[CONF_MODE] + self._attr_pattern = ( + r"[\u0000-\u00ff]*" # Latin-1 + if issubclass(self._device.remote_value.dpt_class, DPTLatin1) + else r"[\u0000-\u007f]*" # ASCII + ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = str(self._device.remote_value.group_address) + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if not self._device.remote_value.readable and ( + last_state := await self.async_get_last_state() + ): + if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self._device.remote_value.value = last_state.state + + @property + def native_value(self) -> str | None: + """Return the value reported by the text.""" + return self._device.message + + async def async_set_value(self, value: str) -> None: + """Change the value.""" + await self._device.set(value) diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 331c85d4065..af87f7d99ba 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -28,30 +28,51 @@ "local_ip": "\u041e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." } }, - "secure_manual": { + "secure_routing_manual": { + "data_description": { + "sync_latency_tolerance": "\u041f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435 \u0435 1000." + } + }, + "secure_tunnel_manual": { "data": { - "user_id": "\u0418\u0414 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f", + "user_id": "ID \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f", "user_password": "\u041f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f" } } } }, "options": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_ip_address": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IPv4 \u0430\u0434\u0440\u0435\u0441." + }, "step": { - "init": { + "manual_tunnel": { "data": { - "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)" + "host": "\u0425\u043e\u0441\u0442", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u043d\u0430 Home Assistant", + "port": "\u041f\u043e\u0440\u0442" }, "data_description": { - "local_ip": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 `0.0.0.0` \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." + } + }, + "routing": { + "data": { + "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u0435\u043d \u0430\u0434\u0440\u0435\u0441", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u043d\u0430 Home Assistant" + }, + "data_description": { + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." + } + }, + "secure_routing_manual": { + "data_description": { + "sync_latency_tolerance": "\u041f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435 \u0435 1000." } }, "tunnel": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442", - "tunneling_type": "KNX \u0442\u0443\u043d\u0435\u043b\u0435\u043d \u0442\u0438\u043f" - } + "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0448\u043b\u044e\u0437 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430." } } } diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index ff83ebec070..ebd50a5810f 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "file_not_found": "No s'ha trobat el fitxer `.knxkeys` especificat a la ruta config/.storage/knx/", + "invalid_backbone_key": "Clau troncal inv\u00e0lida. S'esperen 32 nombres hexadecimals.", "invalid_individual_address": "El valor no coincideix amb el patr\u00f3 d'adre\u00e7a KNX individual.\n'area.line.device'", "invalid_ip_address": "Adre\u00e7a IPv4 inv\u00e0lida.", - "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta." + "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", + "no_router_discovered": "No s'ha descobert cap encaminador ('router') KNXnet/IP a la xarxa.", + "no_tunnel_discovered": "No s'ha trobat cap servidor de tunelitzaci\u00f3 KNX a la xarxa." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipus de connexi\u00f3 KNX" + }, + "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament." + }, "manual_tunnel": { "data": { "host": "Amfitri\u00f3", "local_ip": "IP local de Home Assistant", "port": "Port", + "route_back": "Encaminament de retorn / Mode NAT", "tunneling_type": "Tipus de t\u00fanel KNX" }, "data_description": { "host": "Adre\u00e7a IP del dispositiu de tunelitzaci\u00f3 KNX/IP.", "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic.", - "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP." + "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP.", + "route_back": "Activa-ho si el teun servidor de tunelitzaci\u00f3 KNXnet/IP est\u00e0 darrere una NAT. Nom\u00e9s s'aplica a connexions UDP." }, "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del dispositiu de t\u00fanel." }, @@ -31,7 +42,8 @@ "individual_address": "Adre\u00e7a individual", "local_ip": "IP local de Home Assistant", "multicast_group": "Grup multidifusi\u00f3", - "multicast_port": "Port multidifusi\u00f3" + "multicast_port": "Port multidifusi\u00f3", + "routing_secure": "Utilitza KNX IP Secure" }, "data_description": { "individual_address": "Adre\u00e7a KNX per utilitzar amb Home Assistant, p. ex. `0.0.4`", @@ -39,6 +51,14 @@ }, "description": "Configura les opcions d'encaminament." }, + "secure_key_source": { + "description": "Selecciona com vols configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilitza un fitxer `.knxkeys` que contingui les claus de seguretat IP (IP Secure)", + "secure_routing_manual": "Configura manualment la clau troncal de seguretat IP (IP Secure)", + "secure_tunnel_manual": "Configura manualment les credencials de seguretat IP (IP Secure)" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "Nom del teu fitxer `.knxkeys` (inclosa l'extensi\u00f3)", @@ -50,7 +70,18 @@ }, "description": "Introdueix la informaci\u00f3 del teu fitxer `.knxkeys`." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Clau troncal ('backbone')", + "sync_latency_tolerance": "Toler\u00e0ncia de lat\u00e8ncia de xarxa" + }, + "data_description": { + "backbone_key": "Es pot veure dins l'informe de 'Seguretat' d'un projecte ETS. Per exemple: '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "El valor per defecte \u00e9s 1000." + }, + "description": "Introdueix la informaci\u00f3 de seguretat IP (IP Secure)." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "Contrasenya d'autenticaci\u00f3 del dispositiu", "user_id": "ID d'usuari", @@ -67,7 +98,7 @@ "description": "Selecciona com vols configurar KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Utilitza un fitxer `.knxkeys` que contingui les claus de seguretat IP (IP Secure)", - "secure_manual": "Configura manualment les claus de seguretat IP (IP Secure)" + "secure_tunnel_manual": "Configura manualment les claus de seguretat IP (IP Secure)" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "Connexi\u00f3 t\u00fanel KNX" }, "description": "Selecciona una passarel\u00b7la d'enlla\u00e7 de la llista." - }, - "type": { - "data": { - "connection_type": "Tipus de connexi\u00f3 KNX" - }, - "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament." } } }, "options": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "file_not_found": "No s'ha trobat el fitxer `.knxkeys` especificat a la ruta config/.storage/knx/", + "invalid_backbone_key": "Clau troncal inv\u00e0lida. S'esperen 32 nombres hexadecimals.", + "invalid_individual_address": "El valor no coincideix amb el patr\u00f3 d'adre\u00e7a KNX individual.\n'area.line.device'", + "invalid_ip_address": "Adre\u00e7a IPv4 inv\u00e0lida.", + "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", + "no_router_discovered": "No s'ha descobert cap encaminador ('router') KNXnet/IP a la xarxa.", + "no_tunnel_discovered": "No s'ha trobat cap servidor de tunelitzaci\u00f3 KNX a la xarxa." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "Tipus de connexi\u00f3 KNX", - "individual_address": "Adre\u00e7a individual predeterminada", - "local_ip": "IP local de Home Assistant", - "multicast_group": "Grup multidifusi\u00f3", - "multicast_port": "Port multidifusi\u00f3", "rate_limit": "Freq\u00fc\u00e8ncia m\u00e0xima", "state_updater": "Actualitzador d'estat" }, "data_description": { - "individual_address": "Adre\u00e7a KNX per utilitzar amb Home Assistant, p. ex. `0.0.4`", - "local_ip": "Utilitza `0.0.0.0` per al descobriment autom\u00e0tic.", - "multicast_group": "Utilitzada per a l'encaminament i el descobriment. Per defecte: `224.0.23.12`", - "multicast_port": "Utilitzat per a l'encaminament i el descobriment. Per defecte: `3671`", - "rate_limit": "Telegrames de sortida m\u00e0xims per segon.\nRecomanat: de 20 a 40", + "rate_limit": "Telegrames de sortida m\u00e0xims per segon.\nUtilitza `0` per desactivar la limitaci\u00f3. Recomanat: 0 o, de 20 a 40", "state_updater": "Configuraci\u00f3 predeterminadament per llegir els estats del bus KNX. Si est\u00e0 desactivat, Home Assistant no obtindr\u00e0 activament els estats del bus KNX. Les opcions d'entitat `sync_state` poden substituir-ho." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "Tipus de connexi\u00f3 KNX" + }, + "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament." + }, + "manual_tunnel": { "data": { "host": "Amfitri\u00f3", + "local_ip": "IP local de Home Assistant", "port": "Port", + "route_back": "Encaminament de retorn / Mode NAT", "tunneling_type": "Tipus de t\u00fanel KNX" }, "data_description": { "host": "Adre\u00e7a IP del dispositiu de tunelitzaci\u00f3 KNX/IP.", - "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP." + "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic.", + "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP.", + "route_back": "Activa-ho si el teun servidor de tunelitzaci\u00f3 KNXnet/IP est\u00e0 darrere una NAT. Nom\u00e9s s'aplica a connexions UDP." + }, + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del dispositiu de t\u00fanel." + }, + "options_init": { + "menu_options": { + "communication_settings": "Configuraci\u00f3 de la comunicaci\u00f3", + "connection_type": "Configura la interf\u00edcie KNX" } + }, + "routing": { + "data": { + "individual_address": "Adre\u00e7a individual", + "local_ip": "IP local de Home Assistant", + "multicast_group": "Grup multidifusi\u00f3", + "multicast_port": "Port multidifusi\u00f3", + "routing_secure": "Utilitza KNX IP Secure" + }, + "data_description": { + "individual_address": "Adre\u00e7a KNX per utilitzar amb Home Assistant, p. ex. `0.0.4`", + "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic." + }, + "description": "Configura les opcions d'encaminament." + }, + "secure_key_source": { + "description": "Selecciona com vols configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilitza un fitxer `.knxkeys` que contingui les claus de seguretat IP (IP Secure)", + "secure_routing_manual": "Configura manualment la clau troncal de seguretat IP (IP Secure)", + "secure_tunnel_manual": "Configura manualment les credencials de seguretat IP (IP Secure)" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nom del teu fitxer `.knxkeys` (inclosa l'extensi\u00f3)", + "knxkeys_password": "Contrasenya per desxifrar el fitxer `.knxkeys`." + }, + "data_description": { + "knxkeys_filename": "S'espera que el fitxer es trobi al teu directori de configuraci\u00f3 a `.storage/knx/`.\nA Home Assistant aix\u00f2 estaria a `/config/.storage/knx/`\nExemple: `el_meu_projecte.knxkeys`", + "knxkeys_password": "S'ha definit durant l'exportaci\u00f3 del fitxer des d'ETS." + }, + "description": "Introdueix la informaci\u00f3 del teu fitxer `.knxkeys`." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Clau troncal ('backbone')", + "sync_latency_tolerance": "Toler\u00e0ncia de lat\u00e8ncia de xarxa" + }, + "data_description": { + "backbone_key": "Es pot veure dins l'informe de 'Seguretat' d'un projecte ETS. Per exemple: '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "El valor per defecte \u00e9s 1000." + }, + "description": "Introdueix la informaci\u00f3 de seguretat IP (IP Secure)." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Contrasenya d'autenticaci\u00f3 del dispositiu", + "user_id": "ID d'usuari", + "user_password": "Contrasenya d'usuari" + }, + "data_description": { + "device_authentication": "S'estableix al panell 'IP' de la interf\u00edcie d'ETS.", + "user_id": "Sovint \u00e9s el n\u00famero del t\u00fanel +1. Per tant, 'T\u00fanel 2' tindria l'ID d'usuari '3'.", + "user_password": "Contrasenya per a la connexi\u00f3 t\u00fanel espec\u00edfica configurada al panell 'Propietats' del t\u00fanel a ETS." + }, + "description": "Introdueix la informaci\u00f3 de seguretat IP (IP Secure)." + }, + "secure_tunneling": { + "description": "Selecciona com vols configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilitza un fitxer `.knxkeys` que contingui les claus de seguretat IP (IP Secure)", + "secure_tunnel_manual": "Configura manualment les claus de seguretat IP (IP Secure)" + } + }, + "tunnel": { + "data": { + "gateway": "Connexi\u00f3 t\u00fanel KNX" + }, + "description": "Selecciona una passarel\u00b7la d'enlla\u00e7 de la llista." } } } diff --git a/homeassistant/components/knx/translations/cs.json b/homeassistant/components/knx/translations/cs.json index 90c988aaeac..325e4710145 100644 --- a/homeassistant/components/knx/translations/cs.json +++ b/homeassistant/components/knx/translations/cs.json @@ -4,7 +4,8 @@ "already_configured": "Slu\u017eba je ji\u017e nastavena" }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_ip_address": "Neplatn\u00e1 adresa IPv4." }, "step": { "manual_tunnel": { @@ -12,16 +13,35 @@ "host": "Hostitel", "port": "Port" } + }, + "routing": { + "data_description": { + "individual_address": "Adresa KNX, kterou m\u00e1 pou\u017e\u00edvat Home Assistant, nap\u0159. `0.0.4`" + } } } }, "options": { + "error": { + "invalid_ip_address": "Neplatn\u00e1 adresa IPv4." + }, "step": { - "tunnel": { + "manual_tunnel": { "data": { "host": "Hostitel", "port": "Port" } + }, + "options_init": { + "menu_options": { + "communication_settings": "Nastaven\u00ed komunikace", + "connection_type": "Konfigurace rozhran\u00ed KNX" + } + }, + "routing": { + "data_description": { + "individual_address": "Adresa KNX, kterou m\u00e1 pou\u017e\u00edvat Home Assistant, nap\u0159. `0.0.4`" + } } } } diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 1daffa9c301..9c64e5b6edb 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -6,23 +6,34 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "file_not_found": "Die angegebene `.knxkeys`-Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", + "file_not_found": "Die angegebene `.knxkeys` Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", + "invalid_backbone_key": "Ung\u00fcltiger Backbone-Schl\u00fcssel. 32 Hexadezimalzahlen erwartet.", "invalid_individual_address": "Wert ist keine g\u00fcltige physikalische Adresse. 'Bereich.Linie.Teilnehmer'", "invalid_ip_address": "Ung\u00fcltige IPv4 Adresse.", - "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei ist ung\u00fcltig." + "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys` Datei ist ung\u00fcltig.", + "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", + "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX-Verbindungstyp" + }, + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "Lokale IP von Home Assistant", "port": "Port", + "route_back": "Zur\u00fcckrouten / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" }, "data_description": { "host": "IP-Adresse der KNX/IP-Tunneling Schnittstelle.", "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden.", - "port": "Port der KNX/IP-Tunneling Schnittstelle." + "port": "Port der KNX/IP-Tunneling Schnittstelle.", + "route_back": "Aktiviere diese Option, wenn sich dein KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." }, "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." }, @@ -30,27 +41,47 @@ "data": { "individual_address": "Physikalische Adresse", "local_ip": "Lokale IP von Home Assistant", - "multicast_group": "Multicast-Gruppe", - "multicast_port": "Multicast-Port" + "multicast_group": "Multicast Gruppe", + "multicast_port": "Multicast Port", + "routing_secure": "KNX IP-Secure verwenden" }, "data_description": { - "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z. B. \u201e0.0.4\u201c.", + "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z.B. \u201e0.0.4\u201c.", "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden." }, "description": "Bitte konfiguriere die Routing-Optionen." }, + "secure_key_source": { + "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", + "menu_options": { + "secure_knxkeys": "Verwenden einer \"knxkeys\"-Datei mit IP-Secure-Schl\u00fcsseln", + "secure_routing_manual": "IP-Secure Backbone-Schl\u00fcssel manuell konfigurieren", + "secure_tunnel_manual": "IP-Secure-Anmeldeinformationen manuell konfigurieren" + } + }, "secure_knxkeys": { "data": { - "knxkeys_filename": "Der Dateiname deiner `.knxkeys`-Datei (einschlie\u00dflich Erweiterung)", - "knxkeys_password": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei" + "knxkeys_filename": "Der Dateiname deiner `.knxkeys` Datei (einschlie\u00dflich Erweiterung)", + "knxkeys_password": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys` Datei" }, "data_description": { "knxkeys_filename": "Die Datei wird in deinem Konfigurationsverzeichnis unter `.storage/knx/` erwartet.\nIm Home Assistant OS w\u00e4re dies `/config/.storage/knx/`\nBeispiel: `my_project.knxkeys`", "knxkeys_password": "Dies wurde beim Exportieren der Datei aus ETS gesetzt." }, - "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys`-Datei ein." + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone-Schl\u00fcssel", + "sync_latency_tolerance": "Netzwerklatenztoleranz" + }, + "data_description": { + "backbone_key": "Kann im Bericht \"Sicherheit\" eines ETS-Projekts eingesehen werden. z.B. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Der Standardwert ist 1000." + }, + "description": "Bitte gib deine IP-Secure Informationen ein." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "Ger\u00e4te-Authentifizierungscode", "user_id": "Benutzer-ID", @@ -66,8 +97,8 @@ "secure_tunneling": { "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", "menu_options": { - "secure_knxkeys": "Verwende eine `.knxkeys`-Datei, die IP-Secure-Schl\u00fcssel enth\u00e4lt", - "secure_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + "secure_knxkeys": "Verwende eine `.knxkeys` Datei, die IP-Secure Schl\u00fcssel enth\u00e4lt", + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "KNX Tunnel Verbindung" }, "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." - }, - "type": { - "data": { - "connection_type": "KNX-Verbindungstyp" - }, - "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." } } }, "options": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "file_not_found": "Die angegebene `.knxkeys` Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", + "invalid_backbone_key": "Ung\u00fcltiger Backbone-Schl\u00fcssel. 32 Hexadezimalzahlen erwartet.", + "invalid_individual_address": "Wert ist keine g\u00fcltige physikalische Adresse. 'Bereich.Linie.Teilnehmer'", + "invalid_ip_address": "Ung\u00fcltige IPv4 Adresse.", + "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys` Datei ist ung\u00fcltig.", + "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", + "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "KNX-Verbindungstyp", - "individual_address": "Standard physikalische Adresse", - "local_ip": "Lokale IP von Home Assistant", - "multicast_group": "Multicast-Gruppe", - "multicast_port": "Multicast-Port", - "rate_limit": "Telegrammdrossel", + "rate_limit": "Ratenlimit", "state_updater": "Status-Updater" }, "data_description": { - "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z.\u00a0B. \u201e0.0.4\u201c.", - "local_ip": "Verwende \"0.0.0.0\" f\u00fcr die automatische Erkennung.", - "multicast_group": "Wird f\u00fcr Routing und Netzwerkerkennung verwendet. Standard: `224.0.23.12`", - "multicast_port": "Wird f\u00fcr Routing und Netzwerkerkennung verwendet. Standard: \u201e3671\u201c.", - "rate_limit": "Maximal gesendete Telegramme pro Sekunde.\nEmpfohlen: 20 bis 40", + "rate_limit": "Maximal ausgehende Telegramme pro Sekunde.\n `0`, um das Limit zu deaktivieren. Empfohlen: 0 oder 20 bis 40", "state_updater": "Standardeinstellung f\u00fcr das Lesen von Zust\u00e4nden aus dem KNX-Bus. Wenn diese Option deaktiviert ist, wird der Home Assistant den Zustand der Entit\u00e4ten nicht aktiv vom KNX-Bus abrufen. Kann durch die Entity-Optionen `sync_state` au\u00dfer Kraft gesetzt werden." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "KNX-Verbindungstyp" + }, + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + }, + "manual_tunnel": { "data": { "host": "Host", + "local_ip": "Lokale IP von Home Assistant", "port": "Port", + "route_back": "Zur\u00fcckrouten / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" }, "data_description": { "host": "IP-Adresse der KNX/IP-Tunneling Schnittstelle.", - "port": "Port der KNX/IP-Tunneling Schnittstelle." + "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden.", + "port": "Port der KNX/IP-Tunneling Schnittstelle.", + "route_back": "Aktiviere diese Option, wenn sich dein KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." + }, + "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." + }, + "options_init": { + "menu_options": { + "communication_settings": "Kommunikationseinstellungen", + "connection_type": "KNX-Schnittstelle konfigurieren" } + }, + "routing": { + "data": { + "individual_address": "Physikalische Adresse", + "local_ip": "Lokale IP von Home Assistant", + "multicast_group": "Multicast Gruppe", + "multicast_port": "Multicast Port", + "routing_secure": "KNX IP-Secure verwenden" + }, + "data_description": { + "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z.B. \u201e0.0.4\u201c.", + "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden." + }, + "description": "Bitte konfiguriere die Routing-Optionen." + }, + "secure_key_source": { + "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", + "menu_options": { + "secure_knxkeys": "Verwenden einer \"knxkeys\"-Datei mit IP-Secure-Schl\u00fcsseln", + "secure_routing_manual": "IP-Secure Backbone-Schl\u00fcssel manuell konfigurieren", + "secure_tunnel_manual": "IP-Secure-Anmeldeinformationen manuell konfigurieren" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Der Dateiname deiner `.knxkeys` Datei (einschlie\u00dflich Erweiterung)", + "knxkeys_password": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys` Datei" + }, + "data_description": { + "knxkeys_filename": "Die Datei wird in deinem Konfigurationsverzeichnis unter `.storage/knx/` erwartet.\nIm Home Assistant OS w\u00e4re dies `/config/.storage/knx/`\nBeispiel: `my_project.knxkeys`", + "knxkeys_password": "Dies wurde beim Exportieren der Datei aus ETS gesetzt." + }, + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone-Schl\u00fcssel", + "sync_latency_tolerance": "Netzwerklatenztoleranz" + }, + "data_description": { + "backbone_key": "Kann im Bericht \"Sicherheit\" eines ETS-Projekts eingesehen werden. z.B. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Der Standardwert ist 1000." + }, + "description": "Bitte gib deine IP-Secure Informationen ein." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Ger\u00e4te-Authentifizierungscode", + "user_id": "Benutzer-ID", + "user_password": "Benutzer-Passwort" + }, + "data_description": { + "device_authentication": "Dies wird im Feld \"IP\" der Schnittstelle in ETS eingestellt.", + "user_id": "Dies ist oft die Tunnelnummer +1. \u201eTunnel 2\u201c h\u00e4tte also die Benutzer-ID \u201e3\u201c.", + "user_password": "Passwort f\u00fcr die spezifische Tunnelverbindung, die im Bereich \u201eEigenschaften\u201c des Tunnels in ETS festgelegt wurde." + }, + "description": "Bitte gib deine IP-Secure Informationen ein." + }, + "secure_tunneling": { + "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", + "menu_options": { + "secure_knxkeys": "Verwende eine `.knxkeys` Datei, die IP-Secure Schl\u00fcssel enth\u00e4lt", + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + } + }, + "tunnel": { + "data": { + "gateway": "KNX Tunnel Verbindung" + }, + "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." } } } diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 30047781c26..660ce9218f9 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -9,29 +9,39 @@ "file_not_found": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae config/.storage/knx/", "invalid_individual_address": "\u0397 \u03c4\u03b9\u03bc\u03ae \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bc\u03bf\u03c4\u03af\u03b2\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX.\n \"area.line.device\"", "invalid_ip_address": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPv4.", - "invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2." + "invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2.", + "no_router_discovered": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 KNXnet/IP \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", + "no_tunnel_discovered": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2." }, "step": { + "connection_type": { + "data": { + "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + }, "manual_tunnel": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7)", "port": "\u0398\u03cd\u03c1\u03b1", + "route_back": "\u03a0\u03af\u03c3\u03c9 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae / \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, "data_description": { "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP.", "local_ip": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.", - "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP." + "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX/IP.", + "route_back": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b5\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 KNXnet/IP tunneling \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c0\u03af\u03c3\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf NAT. \u0399\u03c3\u03c7\u03cd\u03b5\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 UDP." }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03ac\u03c2 \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." }, "routing": { "data": { - "individual_address": "\u0391\u03c4\u03bf\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2", - "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7)", - "multicast_group": "\u0397 \u03bf\u03bc\u03ac\u03b4\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b5\u03ba\u03c0\u03bf\u03bc\u03c0\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7", - "multicast_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" + "individual_address": "\u0391\u03c4\u03bf\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7", + "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant", + "multicast_group": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2", + "multicast_port": "\u0398\u03cd\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b5\u03ba\u03c0\u03bf\u03bc\u03c0\u03ae\u03c2" }, "data_description": { "individual_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant, \u03c0.\u03c7. `0.0.4`.", @@ -41,7 +51,7 @@ }, "secure_knxkeys": { "data": { - "knxkeys_filename": "\u03a4\u03bf \u03c0\u03bb\u03ae\u03c1\u03b5\u03c2 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03c3\u03b1\u03c2", + "knxkeys_filename": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c3\u03b1\u03c2 `.knxkeys` (\u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03b1\u03bd\u03bf\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c0\u03ad\u03ba\u03c4\u03b1\u03c3\u03b7\u03c2)", "knxkeys_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys" }, "data_description": { @@ -50,24 +60,24 @@ }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c3\u03b1\u03c2." }, - "secure_manual": { + "secure_tunnel_manual": { "data": { - "device_authentication": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "device_authentication": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "user_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", - "user_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + "user_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "data_description": { "device_authentication": "\u0391\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00abIP\u00bb \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03bf ETS.", - "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03c4\u03bf \u03bd\u03bf\u03cd\u03bc\u03b5\u03c1\u03bf +1 \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2. \u0388\u03c4\u03c3\u03b9, \u03b7 '\u03a3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1 2' \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID '3'.", - "user_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \"\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." + "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 +1. \u0386\u03c1\u03b1 \u03c4\u03bf \"Tunnel 2\" \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID \"3\".", + "user_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00ab\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\u00bb \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP secure." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." }, "secure_tunneling": { "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03ce\u03c2 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf IP Secure.", "menu_options": { - "secure_knxkeys": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP secure", - "secure_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 IP secure \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1" + "secure_knxkeys": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP", + "secure_tunnel_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ce\u03bd \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" } }, "tunnel": { @@ -75,46 +85,107 @@ "gateway": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1." - }, - "type": { - "data": { - "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" - }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 KNX. \n \u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u0397 - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2. \n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03cc \u03c3\u03b1\u03c2 KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2. \n ROUTING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03cc \u03c3\u03b1\u03c2 KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." } } }, "options": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "file_not_found": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae config/.storage/knx/", + "invalid_individual_address": "\u0397 \u03c4\u03b9\u03bc\u03ae \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bc\u03bf\u03c4\u03af\u03b2\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX.\n \"area.line.device\"", + "invalid_ip_address": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPv4.", + "invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2.", + "no_router_discovered": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 KNXnet/IP \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", + "no_tunnel_discovered": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX", - "individual_address": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c6\u03c5\u03c3\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7", - "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant (\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 0.0.0.0.0 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7)", - "multicast_group": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7", - "multicast_port": "\u0398\u03cd\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7", - "rate_limit": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b1 \u03b5\u03be\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03b3\u03c1\u03b1\u03c6\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03bf", - "state_updater": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ac \u03c4\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf KNX Bus" + "rate_limit": "\u038c\u03c1\u03b9\u03bf \u03c0\u03bf\u03c3\u03bf\u03c3\u03c4\u03bf\u03cd", + "state_updater": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03c4\u03ae\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2" }, "data_description": { - "individual_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant, \u03c0.\u03c7. `0.0.4`.", - "local_ip": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 `0.0.0.0.0` \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7.", - "multicast_group": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7. \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: `224.0.23.12`", - "multicast_port": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7. \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: `3671`", - "rate_limit": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b1 \u03b5\u03be\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03b3\u03c1\u03b1\u03c6\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03bf.\n \u03a0\u03c1\u03bf\u03c4\u03b5\u03af\u03bd\u03b5\u03c4\u03b1\u03b9: 20 \u03ad\u03c9\u03c2 40", - "state_updater": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX. \u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf, \u03c4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ac \u03b5\u03bd\u03b5\u03c1\u03b3\u03ac \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf KNX Bus, \u03bf\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 `sync_state` \u03b4\u03b5\u03bd \u03b8\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ba\u03b1\u03bc\u03af\u03b1 \u03b5\u03c0\u03af\u03b4\u03c1\u03b1\u03c3\u03b7." + "rate_limit": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b1 \u03b5\u03be\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03b3\u03c1\u03b1\u03c6\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03bf.\n \"0\" \u03b3\u03b9\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bf\u03c1\u03af\u03bf\u03c5. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9: 0 \u03ae 20 \u03ad\u03c9\u03c2 40", + "state_updater": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX. \u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf, \u03c4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ac \u03b5\u03bd\u03b5\u03c1\u03b3\u03ac \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf KNX Bus. \u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bc\u03c6\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u00absync_state\u00bb." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + }, + "manual_tunnel": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7)", "port": "\u0398\u03cd\u03c1\u03b1", + "route_back": "\u03a0\u03af\u03c3\u03c9 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae / \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, "data_description": { "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP.", - "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP." + "local_ip": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.", + "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX/IP.", + "route_back": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b5\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 KNXnet/IP tunneling \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c0\u03af\u03c3\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf NAT. \u0399\u03c3\u03c7\u03cd\u03b5\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 UDP." + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + }, + "options_init": { + "menu_options": { + "communication_settings": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2", + "connection_type": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" } + }, + "routing": { + "data": { + "individual_address": "\u0391\u03c4\u03bf\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7", + "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant", + "multicast_group": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2", + "multicast_port": "\u0398\u03cd\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b5\u03ba\u03c0\u03bf\u03bc\u03c0\u03ae\u03c2" + }, + "data_description": { + "individual_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant, \u03c0.\u03c7. `0.0.4`.", + "local_ip": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c3\u03b1\u03c2 `.knxkeys` (\u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03b1\u03bd\u03bf\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c0\u03ad\u03ba\u03c4\u03b1\u03c3\u03b7\u03c2)", + "knxkeys_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys" + }, + "data_description": { + "knxkeys_filename": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c3\u03c4\u03bf `.storage/knx/`.\n \u03a3\u03c4\u03bf Home Assistant OS \u03b1\u03c5\u03c4\u03cc \u03b8\u03b1 \u03ae\u03c4\u03b1\u03bd `/config/.storage/knx/`\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: `my_project.knxkeys`", + "knxkeys_password": "\u0391\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03be\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03b1\u03c0\u03cc \u03c4\u03bf ETS." + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c3\u03b1\u03c2." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "user_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "user_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "data_description": { + "device_authentication": "\u0391\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00abIP\u00bb \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03bf ETS.", + "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 +1. \u0386\u03c1\u03b1 \u03c4\u03bf \"Tunnel 2\" \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID \"3\".", + "user_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00ab\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\u00bb \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + }, + "secure_tunneling": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03ce\u03c2 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf IP Secure.", + "menu_options": { + "secure_knxkeys": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP", + "secure_tunnel_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ce\u03bd \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" + } + }, + "tunnel": { + "data": { + "gateway": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1." } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 6dffe059b2a..76ed9ba27b2 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "Failed to connect", "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", + "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", "invalid_ip_address": "Invalid IPv4 address.", - "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong." + "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", + "no_router_discovered": "No KNXnet/IP router was discovered on the network.", + "no_tunnel_discovered": "Could not find a KNX tunneling server on your network." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX Connection Type" + }, + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "Local IP of Home Assistant", "port": "Port", + "route_back": "Route back / NAT mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { "host": "IP address of the KNX/IP tunneling device.", "local_ip": "Leave blank to use auto-discovery.", - "port": "Port of the KNX/IP tunneling device." + "port": "Port of the KNX/IP tunneling device.", + "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections." }, "description": "Please enter the connection information of your tunneling device." }, @@ -31,7 +42,8 @@ "individual_address": "Individual address", "local_ip": "Local IP of Home Assistant", "multicast_group": "Multicast group", - "multicast_port": "Multicast port" + "multicast_port": "Multicast port", + "routing_secure": "Use KNX IP Secure" }, "data_description": { "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", @@ -39,6 +51,14 @@ }, "description": "Please configure the routing options." }, + "secure_key_source": { + "description": "Select how you want to configure KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_routing_manual": "Configure IP secure backbone key manually", + "secure_tunnel_manual": "Configure IP secure credentials manually" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", @@ -50,7 +70,18 @@ }, "description": "Please enter the information for your `.knxkeys` file." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone key", + "sync_latency_tolerance": "Network latency tolerance" + }, + "data_description": { + "backbone_key": "Can be seen in the 'Security' report of an ETS project. Eg. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Default is 1000." + }, + "description": "Please enter your IP secure information." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "Device authentication password", "user_id": "User ID", @@ -67,7 +98,7 @@ "description": "Select how you want to configure KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", - "secure_manual": "Configure IP secure keys manually" + "secure_tunnel_manual": "Configure IP secure keys manually" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "KNX Tunnel Connection" }, "description": "Please select a gateway from the list." - }, - "type": { - "data": { - "connection_type": "KNX Connection Type" - }, - "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." } } }, "options": { + "error": { + "cannot_connect": "Failed to connect", + "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", + "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", + "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", + "invalid_ip_address": "Invalid IPv4 address.", + "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", + "no_router_discovered": "No KNXnet/IP router was discovered on the network.", + "no_tunnel_discovered": "Could not find a KNX tunneling server on your network." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "KNX Connection Type", - "individual_address": "Default individual address", - "local_ip": "Local IP of Home Assistant", - "multicast_group": "Multicast group", - "multicast_port": "Multicast port", "rate_limit": "Rate limit", "state_updater": "State updater" }, "data_description": { - "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", - "local_ip": "Use `0.0.0.0` for auto-discovery.", - "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", - "multicast_port": "Used for routing and discovery. Default: `3671`", - "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40", + "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40", "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "KNX Connection Type" + }, + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." + }, + "manual_tunnel": { "data": { "host": "Host", + "local_ip": "Local IP of Home Assistant", "port": "Port", + "route_back": "Route back / NAT mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { "host": "IP address of the KNX/IP tunneling device.", - "port": "Port of the KNX/IP tunneling device." + "local_ip": "Leave blank to use auto-discovery.", + "port": "Port of the KNX/IP tunneling device.", + "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections." + }, + "description": "Please enter the connection information of your tunneling device." + }, + "options_init": { + "menu_options": { + "communication_settings": "Communication settings", + "connection_type": "Configure KNX interface" } + }, + "routing": { + "data": { + "individual_address": "Individual address", + "local_ip": "Local IP of Home Assistant", + "multicast_group": "Multicast group", + "multicast_port": "Multicast port", + "routing_secure": "Use KNX IP Secure" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Leave blank to use auto-discovery." + }, + "description": "Please configure the routing options." + }, + "secure_key_source": { + "description": "Select how you want to configure KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_routing_manual": "Configure IP secure backbone key manually", + "secure_tunnel_manual": "Configure IP secure credentials manually" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", + "knxkeys_password": "The password to decrypt the `.knxkeys` file" + }, + "data_description": { + "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", + "knxkeys_password": "This was set when exporting the file from ETS." + }, + "description": "Please enter the information for your `.knxkeys` file." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone key", + "sync_latency_tolerance": "Network latency tolerance" + }, + "data_description": { + "backbone_key": "Can be seen in the 'Security' report of an ETS project. Eg. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Default is 1000." + }, + "description": "Please enter your IP secure information." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Device authentication password", + "user_id": "User ID", + "user_password": "User password" + }, + "data_description": { + "device_authentication": "This is set in the 'IP' panel of the interface in ETS.", + "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", + "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS." + }, + "description": "Please enter your IP secure information." + }, + "secure_tunneling": { + "description": "Select how you want to configure KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_tunnel_manual": "Configure IP secure keys manually" + } + }, + "tunnel": { + "data": { + "gateway": "KNX Tunnel Connection" + }, + "description": "Please select a gateway from the list." } } } diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 19de37aaf56..f710dad5597 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "No se pudo conectar", "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", + "invalid_backbone_key": "Clave de red troncal no v\u00e1lida. Se esperan 32 n\u00fameros hexadecimales.", "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", - "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta." + "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", + "no_router_discovered": "No se ha descubierto ning\u00fan router KNXnet/IP en la red.", + "no_tunnel_discovered": "No se pudo encontrar un servidor de t\u00fanel KNX en tu red." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipo de conexi\u00f3n KNX" + }, + "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad a tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a su tus KNX a trav\u00e9s del enrutamiento." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "IP local de Home Assistant", "port": "Puerto", + "route_back": "Ruta de regreso / modo NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { - "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", + "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico.", - "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." + "port": "Puerto del dispositivo de t\u00fanel KNX/IP.", + "route_back": "Habilitar si tu servidor de t\u00fanel IP/KNXnet est\u00e1 detr\u00e1s de NAT. Solo aplica para conexiones UDP." }, "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." }, @@ -31,7 +42,8 @@ "individual_address": "Direcci\u00f3n individual", "local_ip": "IP local de Home Assistant", "multicast_group": "Grupo multicast", - "multicast_port": "Puerto multicast" + "multicast_port": "Puerto multicast", + "routing_secure": "Utilizar KNX IP Secure" }, "data_description": { "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", @@ -39,6 +51,14 @@ }, "description": "Por favor, configura las opciones de enrutamiento." }, + "secure_key_source": { + "description": "Selecciona c\u00f3mo deseas configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", + "secure_routing_manual": "Configurar la clave de red troncal segura de IP manualmente", + "secure_tunnel_manual": "Configurar las credenciales seguras de IP manualmente" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "El nombre de tu archivo `.knxkeys` (incluyendo la extensi\u00f3n)", @@ -50,7 +70,18 @@ }, "description": "Por favor, introduce la informaci\u00f3n de tu archivo `.knxkeys`." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Clave de la red troncal", + "sync_latency_tolerance": "Tolerancia a la latencia de red" + }, + "data_description": { + "backbone_key": "Se puede ver en el informe de 'Seguridad' de un proyecto ETS. Ej. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "El valor predeterminado es 1000." + }, + "description": "Por favor, introduce tu informaci\u00f3n de IP segura." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "Contrase\u00f1a de autenticaci\u00f3n del dispositivo", "user_id": "ID de usuario", @@ -67,7 +98,7 @@ "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", - "secure_manual": "Configurar claves seguras de IP manualmente" + "secure_tunnel_manual": "Configurar claves seguras de IP manualmente" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "Conexi\u00f3n de t\u00fanel KNX" }, "description": "Por favor, selecciona una puerta de enlace de la lista." - }, - "type": { - "data": { - "connection_type": "Tipo de conexi\u00f3n KNX" - }, - "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad con tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s del enrutamiento." } } }, "options": { + "error": { + "cannot_connect": "No se pudo conectar", + "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", + "invalid_backbone_key": "Clave de red troncal no v\u00e1lida. Se esperan 32 n\u00fameros hexadecimales.", + "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", + "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", + "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", + "no_router_discovered": "No se ha descubierto ning\u00fan router KNXnet/IP en la red.", + "no_tunnel_discovered": "No se pudo encontrar un servidor de t\u00fanel KNX en tu red." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "Tipo de conexi\u00f3n KNX", - "individual_address": "Direcci\u00f3n individual predeterminada", - "local_ip": "IP local de Home Assistant", - "multicast_group": "Grupo multicast", - "multicast_port": "Puerto multicast", - "rate_limit": "Frecuencia m\u00e1xima", + "rate_limit": "L\u00edmite de tasa", "state_updater": "Actualizador de estado" }, "data_description": { - "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", - "local_ip": "Usar `0.0.0.0` para el descubrimiento autom\u00e1tico.", - "multicast_group": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", - "multicast_port": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `3671`", - "rate_limit": "N\u00famero m\u00e1ximo de telegramas salientes por segundo.\nRecomendado: 20 a 40", + "rate_limit": "N\u00famero m\u00e1ximo de telegramas salientes por segundo.\n`0` para deshabilitar el l\u00edmite. Recomendado: 0 o 20 a 40", "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente los estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." } }, + "connection_type": { + "data": { + "connection_type": "Tipo de conexi\u00f3n KNX" + }, + "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad a tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a su tus KNX a trav\u00e9s del enrutamiento." + }, + "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "IP local de Home Assistant", + "port": "Puerto", + "route_back": "Ruta de regreso / modo NAT", + "tunneling_type": "Tipo de t\u00fanel KNX" + }, + "data_description": { + "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico.", + "port": "Puerto del dispositivo de t\u00fanel KNX/IP.", + "route_back": "Habilitar si tu servidor de t\u00fanel IP/KNXnet est\u00e1 detr\u00e1s de NAT. Solo aplica para conexiones UDP." + }, + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." + }, + "options_init": { + "menu_options": { + "communication_settings": "Configuraci\u00f3n de comunicaci\u00f3n", + "connection_type": "Configurar interfaz KNX" + } + }, + "routing": { + "data": { + "individual_address": "Direcci\u00f3n individual", + "local_ip": "IP local de Home Assistant", + "multicast_group": "Grupo multicast", + "multicast_port": "Puerto multicast", + "routing_secure": "Utilizar KNX IP Secure" + }, + "data_description": { + "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico." + }, + "description": "Por favor, configura las opciones de enrutamiento." + }, + "secure_key_source": { + "description": "Selecciona c\u00f3mo deseas configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", + "secure_routing_manual": "Configurar la clave de red troncal segura de IP manualmente", + "secure_tunnel_manual": "Configurar las credenciales seguras de IP manualmente" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "El nombre de tu archivo `.knxkeys` (incluyendo la extensi\u00f3n)", + "knxkeys_password": "Contrase\u00f1a para descifrar el archivo `.knxkeys`." + }, + "data_description": { + "knxkeys_filename": "Se espera que el archivo se encuentre en tu directorio de configuraci\u00f3n en `.storage/knx/`.\nEn Home Assistant OS ser\u00eda `/config/.storage/knx/`\nEjemplo: `mi_proyecto.knxkeys`", + "knxkeys_password": "Esto se configur\u00f3 al exportar el archivo desde ETS." + }, + "description": "Por favor, introduce la informaci\u00f3n de tu archivo `.knxkeys`." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Clave de la red troncal", + "sync_latency_tolerance": "Tolerancia a la latencia de red" + }, + "data_description": { + "backbone_key": "Se puede ver en el informe de 'Seguridad' de un proyecto ETS. Ej. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "El valor predeterminado es 1000." + }, + "description": "Por favor, introduce tu informaci\u00f3n de IP segura." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Contrase\u00f1a de autenticaci\u00f3n del dispositivo", + "user_id": "ID de usuario", + "user_password": "Contrase\u00f1a de usuario" + }, + "data_description": { + "device_authentication": "Esto se configura en el panel 'IP' de la interfaz en ETS.", + "user_id": "Este suele ser el n\u00famero de t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda ID de usuario '3'.", + "user_password": "Contrase\u00f1a para la conexi\u00f3n de t\u00fanel espec\u00edfica establecida en el panel 'Propiedades' del t\u00fanel en ETS." + }, + "description": "Por favor, introduce tu informaci\u00f3n de IP segura." + }, + "secure_tunneling": { + "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", + "secure_tunnel_manual": "Configurar claves seguras de IP manualmente" + } + }, "tunnel": { "data": { - "host": "Host", - "port": "Puerto", - "tunneling_type": "Tipo de t\u00fanel KNX" + "gateway": "Conexi\u00f3n de t\u00fanel KNX" }, - "data_description": { - "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", - "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." - } + "description": "Por favor, selecciona una puerta de enlace de la lista." } } } diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index fe60f5404de..1efd61e02ab 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "file_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", + "invalid_backbone_key": "Kehtetu magistraalv\u00f5ti. Eeldatakse 32 kuueteistk\u00fcmnendarvu.", "invalid_individual_address": "V\u00e4\u00e4rtus ei \u00fchti KNX-i individuaalse aadressi mustriga.\n 'area.line.device'", "invalid_ip_address": "Kehtetu IPv4 aadress.", - "invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale." + "invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", + "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", + "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" + }, + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "Home Assistanti kohalik IP aadress", "port": "Port", + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", "tunneling_type": "KNX tunneli t\u00fc\u00fcp" }, "data_description": { "host": "KNX/IP tunneldusseadme IP-aadress.", "local_ip": "Automaatse avastamise kasutamiseks j\u00e4ta t\u00fchjaks.", - "port": "KNX/IP-tunneldusseadme port." + "port": "KNX/IP-tunneldusseadme port.", + "route_back": "Luba, kui KNXneti/IP tunneldusserver on NAT-i taga. Kehtib ainult UDP-\u00fchenduste puhul." }, "description": "Sisesta tunneldamisseadme \u00fchenduse teave." }, @@ -31,7 +42,8 @@ "individual_address": "Individuaalne aadress", "local_ip": "Home Assistanti kohalik IP aadress", "multicast_group": "Multicast grupp", - "multicast_port": "Mulicasti port" + "multicast_port": "Mulicasti port", + "routing_secure": "Kasuta KNX IP Secure'i" }, "data_description": { "individual_address": "Home Assistantis kasutatav KNX-aadress, nt \"0.0.4\".", @@ -39,6 +51,14 @@ }, "description": "Konfigureeri marsruutimissuvandid." }, + "secure_key_source": { + "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", + "menu_options": { + "secure_knxkeys": "Kasuta knxkeys faili mis sisaldab IP Secure teavet.", + "secure_routing_manual": "Seadista IP secure magistraalv\u00f5ti k\u00e4sitsi", + "secure_tunnel_manual": "Seadista IP secure mandaadid k\u00e4sitsi" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "`.nxkeys` faili t\u00e4ielik nimi (koos laiendiga)", @@ -50,7 +70,18 @@ }, "description": "Sisesta oma `.knxkeys` faili teave." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone v\u00f5ti", + "sync_latency_tolerance": "V\u00f5rguviivituse taluvus" + }, + "data_description": { + "backbone_key": "Kuvatakse ETS projekti 'Turvalisus' vaates. N\u00e4iteks '0011223344...'", + "sync_latency_tolerance": "Vaikev\u00e4\u00e4rtus on 1000." + }, + "description": "Sisesta IP Secure teave." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "Seadme autentimise parool", "user_id": "Kasutaja ID", @@ -61,13 +92,13 @@ "user_id": "See on sageli tunneli number +1. Nii et tunnel 2 oleks kasutaja ID-ga 3.", "user_password": "Konkreetse tunneli\u00fchenduse parool, mis on m\u00e4\u00e4ratud ETS-i tunneli paneelil \u201eAtribuudid\u201d." }, - "description": "Sisesta IP Secure teave." + "description": "Sisesta oma IP secure teave." }, "secure_tunneling": { "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", "menu_options": { "secure_knxkeys": "Kasuta knxkeys fail, mis sisaldab IP Secure teavet.", - "secure_manual": "IP Secure v\u00f5tmete k\u00e4sitsi seadistamine" + "secure_tunnel_manual": "IP Secure v\u00f5tmete k\u00e4sitsi seadistamine" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "KNX tunneli \u00fchendus" }, "description": "Vali loendist l\u00fc\u00fcs." - }, - "type": { - "data": { - "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" - }, - "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." } } }, "options": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "file_not_found": "M\u00e4\u00e4ratud kirjet '.knxkeys' ei leitud asukohast config/.storage/knx/", + "invalid_backbone_key": "Kehtetu magistraalv\u00f5ti. Eeldatakse 32 kuueteistk\u00fcmnendarvu.", + "invalid_individual_address": "V\u00e4\u00e4rtuse mall ei vasta KNX seadme \u00fcksuse aadressile.\n'area.line.device'", + "invalid_ip_address": "Vigane IPv4 aadress", + "invalid_signature": "'.knxkeys' kirje dekr\u00fcptimisv\u00f5ti on vale.", + "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", + "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp", - "individual_address": "Vaikimisi individuaalne aadress", - "local_ip": "Home Assistanti kohalik IP aadress", - "multicast_group": "Multicast grupp", - "multicast_port": "Mulicasti port", "rate_limit": "Teavituste m\u00e4\u00e4r", "state_updater": "Oleku uuendaja" }, "data_description": { - "individual_address": "Home Assistantis kasutatav KNX-aadress, nt \"0.0.4\".", - "local_ip": "Automaatse tuvastamise jaoks kasuta `0.0.0.0.0`.", - "multicast_group": "Kasutatakse marsruutimiseks ja avastamiseks. Vaikimisi: \"224.0.23.12\"", - "multicast_port": "Kasutatakse marsruutimiseks ja avastamiseks. Vaikev\u00e4\u00e4rtus: \"3671\"", - "rate_limit": "Maksimaalne v\u00e4ljaminevate telegrammide arv sekundis.\nSoovitatav: 20 kuni 40", + "rate_limit": "Maksimaalne v\u00e4ljaminevate telegrammide arv sekundis. '0 piirangu eemaldamiseks. Soovitatav: 20 kuni 40", "state_updater": "M\u00e4\u00e4ra KNX siini olekute lugemise vaikev\u00e4\u00e4rtused. Kui see on keelatud, ei too Home Assistant aktiivselt olemi olekuid KNX siinilt. Saab alistada olemivalikute s\u00fcnkroonimise_olekuga." } }, + "connection_type": { + "data": { + "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" + }, + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + }, + "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "Home Assistanti kohtv\u00f5rgu IP", + "port": "Port", + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", + "tunneling_type": "KNX tunneli t\u00fc\u00fcp" + }, + "data_description": { + "host": "KNX/IP tunneldusseadme IP aadress.", + "local_ip": "Automaatseks tuvastamiseks j\u00e4ta t\u00fchjaks.", + "port": "KNX/IP tunneldusseadme port.", + "route_back": "Luba kui KNXnet/IP server on NAT-i taga. Kehtib ainult UDP \u00fchendustele." + }, + "description": "Sisesta tunnel\u00fchenduse parameetrid." + }, + "options_init": { + "menu_options": { + "communication_settings": "\u00dchenduse seaded", + "connection_type": "Seadista KNX liides" + } + }, + "routing": { + "data": { + "individual_address": "\u00dcksuse aadress", + "local_ip": "Home Assistati kohtv\u00f5rgu IP aadress", + "multicast_group": "Multicasti grupp", + "multicast_port": "Multicasti port", + "routing_secure": "Kasuta KNX IP Secure'i" + }, + "data_description": { + "individual_address": "Home Assistantis kasutatav KNX aadress, n\u00e4iteks '0.0.4''", + "local_ip": "Automaatseks tuvastamiseks j\u00e4ta t\u00fchjaks." + }, + "description": "Seadista marsruutimine" + }, + "secure_key_source": { + "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", + "menu_options": { + "secure_knxkeys": "Kasuta knxkeys faili mis sisaldab IP Secure teavet.", + "secure_routing_manual": "Seadista IP secure magistraalv\u00f5ti k\u00e4sitsi", + "secure_tunnel_manual": "Seadista IP secure mandaadid k\u00e4sitsi" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "'.knxkeys' kirje nimi (koos laiendiga)", + "knxkeys_password": "Kirje '.knxkeys' dekr\u00fcptimise v\u00f5ti" + }, + "data_description": { + "knxkeys_filename": "See kirje peaks asuma seadete kaustas '.storage/knx/'.\nHome Assistant OS puhul oleks see 'config/.storage/knx/'\nN\u00e4iteks: 'my_project.knxkeys'", + "knxkeys_password": "See saadi kirje eksportisel ETS-ist." + }, + "description": "Sisesta oma '.knxkeys' kirje teave" + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone v\u00f5ti", + "sync_latency_tolerance": "V\u00f5rguviivituse taluvus" + }, + "data_description": { + "backbone_key": "Kuvatakse ETS projekti 'Turvalisus' vaates. N\u00e4iteks '0011223344...'", + "sync_latency_tolerance": "Vaikev\u00e4\u00e4rtus on 1000." + }, + "description": "Sisesta IP Secure teave." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Seadme tuvastamise salas\u00f5na", + "user_id": "Kasutaja ID", + "user_password": "Kasutaja salas\u00f5na" + }, + "data_description": { + "device_authentication": "Seda saab seada ETS liidese 'IP' paneelil", + "user_id": "See on tavaliselt tunneli number+1. Seega 'Tunnel 2' on kasutaja ID-ga '3'.", + "user_password": "Konkreetse tunneli\u00fchenduse parool, mis on m\u00e4\u00e4ratud ETS-i tunneli paneelil \u201eAtribuudid\u201d." + }, + "description": "Sisesta IP secure teave." + }, + "secure_tunneling": { + "description": "Vali kuidas seadistada KNX/IP Secure", + "menu_options": { + "secure_knxkeys": "Kasuta IP secure jaoks kirjet '.knxkeys'", + "secure_tunnel_manual": "Seadista IP secure v\u00f5tmed k\u00e4sitsi" + } + }, "tunnel": { "data": { - "host": "Host", - "port": "Port", - "tunneling_type": "KNX tunneli t\u00fc\u00fcp" + "gateway": "KNX tunnel\u00fchendus" }, - "data_description": { - "host": "KNX/IP tunneldusseadme IP-aadress.", - "port": "KNX/IP-tunneldusseadme port." - } + "description": "Vali nimekirjast l\u00fc\u00fcs" } } } diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 7a1e8e88698..e4bd4d4b059 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -12,6 +12,11 @@ "invalid_signature": "Le mot de passe pour d\u00e9chiffrer le fichier `.knxkeys` est erron\u00e9." }, "step": { + "connection_type": { + "data": { + "connection_type": "Type de connexion KNX" + } + }, "manual_tunnel": { "data": { "host": "H\u00f4te", @@ -50,11 +55,14 @@ }, "description": "Veuillez saisir les informations relatives \u00e0 votre fichier `.knxkeys`." }, - "secure_manual": { + "secure_routing_manual": { + "description": "Veuillez saisir vos informations de s\u00e9curit\u00e9 IP." + }, + "secure_tunnel_manual": { "data": { - "device_authentication": "Mot de passe d'authentification de l'appareil", - "user_id": "ID de l'utilisateur", - "user_password": "Mot de passe de l'utilisateur" + "device_authentication": "Mot de passe d\u2019authentification de l\u2019appareil", + "user_id": "ID de l\u2019utilisateur", + "user_password": "Mot de passe de l\u2019utilisateur" }, "data_description": { "device_authentication": "D\u00e9fini dans le panneau \u00ab\u00a0IP\u00a0\u00bb de l'interface dans ETS.", @@ -67,7 +75,7 @@ "description": "S\u00e9lectionnez la mani\u00e8re dont vous souhaitez configurer la s\u00e9curit\u00e9 IP de KNX.", "menu_options": { "secure_knxkeys": "Utiliser un fichier `.knxkeys` contenant les cl\u00e9s de s\u00e9curit\u00e9 IP", - "secure_manual": "Configurer manuellement les cl\u00e9s de s\u00e9curit\u00e9 IP" + "secure_tunnel_manual": "Configurer manuellement les cl\u00e9s de s\u00e9curit\u00e9 IP" } }, "tunnel": { @@ -75,46 +83,95 @@ "gateway": "Connexion tunnel KNX" }, "description": "Veuillez s\u00e9lectionner une passerelle dans la liste." - }, - "type": { - "data": { - "connection_type": "Type de connexion KNX" - }, - "description": "Veuillez saisir le type de connexion que nous devons utiliser pour votre connexion KNX.\n AUTOMATIQUE - L'int\u00e9gration prend en charge la connectivit\u00e9 \u00e0 votre bus KNX en effectuant un scan de passerelle.\n TUNNELING - L'int\u00e9gration se connectera \u00e0 votre bus KNX via tunneling.\n ROUTAGE - L'int\u00e9gration se connectera \u00e0 votre bus KNX via le routage." } } }, "options": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "file_not_found": "Le fichier `.knxkeys` sp\u00e9cifi\u00e9 n'a pas \u00e9t\u00e9 trouv\u00e9 dans config/.storage/knx/", + "invalid_individual_address": "La valeur de l'adresse individuelle KNX ne correspond pas au mod\u00e8le.\n'area.line.device'", + "invalid_ip_address": "Adresse IPv4 non valide.", + "invalid_signature": "Le mot de passe pour d\u00e9chiffrer le fichier `.knxkeys` est erron\u00e9." + }, "step": { - "init": { + "connection_type": { "data": { - "connection_type": "Type de connexion KNX", - "individual_address": "Adresse individuelle par d\u00e9faut", - "local_ip": "IP locale de Home Assistant", - "multicast_group": "Groupe multicast", - "multicast_port": "Port multicast", - "rate_limit": "Limite d'envoi", - "state_updater": "Mises \u00e0 jour d'\u00e9tat" - }, - "data_description": { - "individual_address": "Adresse KNX que Home Assistant doit utiliser, par exemple `0.0.4`.", - "local_ip": "Utilisez `0.0.0.0` pour la d\u00e9couverte automatique.", - "multicast_group": "Utilis\u00e9 pour le routage et la d\u00e9couverte. Valeur par d\u00e9faut\u00a0: `224.0.23.12`", - "multicast_port": "Utilis\u00e9 pour le routage et la d\u00e9couverte. Valeur par d\u00e9faut\u00a0: `3671`", - "rate_limit": "Nombre maximal de t\u00e9l\u00e9grammes sortants par seconde.\nValeur recommand\u00e9e\u00a0: entre 20 et 40", - "state_updater": "Active ou d\u00e9sactive globalement la lecture des \u00e9tats depuis le bus KNX. Lorsqu'elle est d\u00e9sactiv\u00e9e, Home Assistant ne r\u00e9cup\u00e8re pas activement les \u00e9tats depuis le bus KNX. Peut \u00eatre remplac\u00e9 par les options d'entit\u00e9 `sync_state`." + "connection_type": "Type de connexion KNX" } }, - "tunnel": { + "manual_tunnel": { "data": { "host": "H\u00f4te", + "local_ip": "IP locale de Home Assistant", "port": "Port", "tunneling_type": "Type de tunnel KNX" }, "data_description": { "host": "Adresse IP de l'appareil de tunnel KNX/IP.", + "local_ip": "Laissez le champ vide pour utiliser la d\u00e9couverte automatique.", "port": "Port de l'appareil de tunnel KNX/IP." + }, + "description": "Veuillez saisir les informations de connexion de votre appareil de cr\u00e9ation de tunnel." + }, + "options_init": { + "menu_options": { + "communication_settings": "Param\u00e8tres de communication", + "connection_type": "Configurer l\u2019interface KNX" } + }, + "routing": { + "data": { + "individual_address": "Adresse individuelle", + "local_ip": "IP locale de Home Assistant", + "multicast_group": "Groupe multicast", + "multicast_port": "Port multicast" + }, + "data_description": { + "individual_address": "Adresse KNX que Home Assistant doit utiliser, par exemple `0.0.4`.", + "local_ip": "Laissez le champ vide pour utiliser la d\u00e9couverte automatique." + }, + "description": "Veuillez configurer les options de routage." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Le nom de votre fichier `.knxkeys` (extension incluse)", + "knxkeys_password": "Le mot de passe pour d\u00e9chiffrer le fichier `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Le fichier devrait se trouver dans votre r\u00e9pertoire de configuration dans `.storage/knx/`.\nSous Home Assistant OS, il s'agirait de `/config/.storage/knx/`\nPar exemple\u00a0: `my_project.knxkeys`", + "knxkeys_password": "D\u00e9fini lors de l'exportation du fichier depuis ETS." + }, + "description": "Veuillez saisir les informations relatives \u00e0 votre fichier `.knxkeys`." + }, + "secure_routing_manual": { + "description": "Veuillez saisir vos informations de s\u00e9curit\u00e9 IP." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Mot de passe d\u2019authentification de l\u2019appareil", + "user_id": "ID de l\u2019utilisateur", + "user_password": "Mot de passe de l\u2019utilisateur" + }, + "data_description": { + "device_authentication": "D\u00e9fini dans le panneau \u00ab\u00a0IP\u00a0\u00bb de l'interface dans ETS.", + "user_id": "G\u00e9n\u00e9ralement le num\u00e9ro du tunnel +\u00a01. Par exemple, \u00ab\u00a0Tunnel 2\u00a0\u00bb aurait l'ID utilisateur \u00ab\u00a03\u00a0\u00bb.", + "user_password": "Mot de passe pour la connexion de tunnel sp\u00e9cifique, d\u00e9fini dans le panneau \u00ab\u00a0Propri\u00e9t\u00e9s\u00a0\u00bb du tunnel dans ETS." + }, + "description": "Veuillez saisir vos informations de s\u00e9curit\u00e9 IP." + }, + "secure_tunneling": { + "description": "S\u00e9lectionnez la mani\u00e8re dont vous souhaitez configurer la s\u00e9curit\u00e9 IP de KNX.", + "menu_options": { + "secure_knxkeys": "Utiliser un fichier `.knxkeys` contenant les cl\u00e9s de s\u00e9curit\u00e9 IP", + "secure_tunnel_manual": "Configurer manuellement les cl\u00e9s de s\u00e9curit\u00e9 IP" + } + }, + "tunnel": { + "data": { + "gateway": "Connexion tunnel KNX" + }, + "description": "Veuillez s\u00e9lectionner une passerelle dans la liste." } } } diff --git a/homeassistant/components/knx/translations/he.json b/homeassistant/components/knx/translations/he.json index dea454b5f6c..8bf31c4e7c7 100644 --- a/homeassistant/components/knx/translations/he.json +++ b/homeassistant/components/knx/translations/he.json @@ -21,21 +21,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4", - "multicast_port": "\u05d9\u05e6\u05d9\u05d0\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4" - } - }, - "tunnel": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "port": "\u05e4\u05ea\u05d7\u05d4" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 92411b58312..ab6e3a8c9af 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -9,20 +9,30 @@ "file_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "invalid_individual_address": "Az \u00e9rt\u00e9k nem felel meg a KNX egyedi c\u00edm mint\u00e1j\u00e1nak.\n'area.line.device'", "invalid_ip_address": "\u00c9rv\u00e9nytelen IPv4-c\u00edm.", - "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen." + "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", + "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" + }, + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + }, "manual_tunnel": { "data": { "host": "C\u00edm", "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", "port": "Port", + "route_back": "Vissza\u00fat / NAT m\u00f3d", "tunneling_type": "KNX alag\u00fat t\u00edpusa" }, "data_description": { "host": "A KNX/IP tunnel eszk\u00f6z IP-c\u00edme.", "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen.", - "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma." + "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", + "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." }, "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." }, @@ -50,7 +60,7 @@ }, "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." }, - "secure_manual": { + "secure_tunnel_manual": { "data": { "device_authentication": "Eszk\u00f6z hiteles\u00edt\u00e9si jelsz\u00f3", "user_id": "Felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3", @@ -67,7 +77,7 @@ "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", "menu_options": { "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", - "secure_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa" + "secure_tunnel_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa" } }, "tunnel": { @@ -75,46 +85,107 @@ "gateway": "KNX alag\u00fat (tunnel) kapcsolat" }, "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." - }, - "type": { - "data": { - "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" - }, - "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." } } }, "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "file_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", + "invalid_individual_address": "Az \u00e9rt\u00e9k nem felel meg a KNX egyedi c\u00edm mint\u00e1j\u00e1nak.\n'area.line.device'", + "invalid_ip_address": "\u00c9rv\u00e9nytelen IPv4-c\u00edm.", + "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", + "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa", - "individual_address": "Alap\u00e9rtelmezett egy\u00e9ni c\u00edm", - "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", - "multicast_group": "Multicast csoport", - "multicast_port": "Multicast portsz\u00e1m", "rate_limit": "Lek\u00e9r\u00e9si korl\u00e1toz\u00e1s", "state_updater": "\u00c1llapot friss\u00edt\u0151" }, "data_description": { - "individual_address": "A Home Assistant \u00e1ltal haszn\u00e1land\u00f3 KNX-c\u00edm, pl. \"0.0.4\".", - "local_ip": "Haszn\u00e1lja a `0.0.0.0` c\u00edmet az automatikus felder\u00edt\u00e9shez.", - "multicast_group": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1latos. Alap\u00e9rtelmezett: `224.0.23.12`.", - "multicast_port": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1latos. Alap\u00e9rtelmezett: `3671`", - "rate_limit": "Maxim\u00e1lis kimen\u0151 \u00fczenet m\u00e1sodpercenk\u00e9nt.\nAj\u00e1nlott: 20 \u00e9s 40 k\u00f6z\u00f6tt", + "rate_limit": "Maxim\u00e1lisan kimen\u0151 \u00fczenet m\u00e1sodpercenk\u00e9nt. 0 a kikapcsol\u00e1shoz.\nAj\u00e1nlott: 0, vagy 20 \u00e9s 40 k\u00f6z\u00f6tt", "state_updater": "Alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1s a KNX busz \u00e1llapotainak olvas\u00e1s\u00e1hoz. Ha le va tiltva, Home Assistant nem fog akt\u00edvan lek\u00e9rdezni egys\u00e9g\u00e1llapotokat a KNX buszr\u00f3l. Fel\u00fclb\u00edr\u00e1lhat\u00f3 a `sync_state` entit\u00e1s opci\u00f3kkal." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" + }, + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + }, + "manual_tunnel": { "data": { "host": "C\u00edm", + "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", "port": "Port", + "route_back": "Vissza\u00fat / NAT m\u00f3d", "tunneling_type": "KNX alag\u00fat t\u00edpusa" }, "data_description": { "host": "A KNX/IP tunnel eszk\u00f6z IP-c\u00edme.", - "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma." + "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen.", + "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", + "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." + }, + "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." + }, + "options_init": { + "menu_options": { + "communication_settings": "Kommunik\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sok", + "connection_type": "KNX interf\u00e9sz konfigur\u00e1l\u00e1sa" } + }, + "routing": { + "data": { + "individual_address": "Egy\u00e9ni c\u00edm", + "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", + "multicast_group": "Multicast csoport", + "multicast_port": "Multicast portsz\u00e1m" + }, + "data_description": { + "individual_address": "A Home Assistant \u00e1ltal haszn\u00e1land\u00f3 KNX-c\u00edm, pl. \"0.0.4\".", + "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen." + }, + "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "A '.knxkeys' f\u00e1jl teljes neve (kiterjeszt\u00e9ssel)", + "knxkeys_password": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez sz\u00fcks\u00e9ges jelsz\u00f3" + }, + "data_description": { + "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", + "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." + }, + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Eszk\u00f6z hiteles\u00edt\u00e9si jelsz\u00f3", + "user_id": "Felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3", + "user_password": "Felhaszn\u00e1l\u00f3i jelsz\u00f3" + }, + "data_description": { + "device_authentication": "Ezt az ETS-ben az interf\u00e9sz \"IP\" panelj\u00e9n kell be\u00e1ll\u00edtani.", + "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", + "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." + }, + "description": "K\u00e9rem, adja meg az IP secure adatokat." + }, + "secure_tunneling": { + "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", + "menu_options": { + "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", + "secure_tunnel_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa" + } + }, + "tunnel": { + "data": { + "gateway": "KNX alag\u00fat (tunnel) kapcsolat" + }, + "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." } } } diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index bbf9a1b7862..4e4f17f70bd 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "Gagal terhubung", "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", + "invalid_backbone_key": "Kunci backbone tidak valid. Diharapkan 32 angka heksadesimal.", "invalid_individual_address": "Nilai tidak cocok dengan pola untuk alamat individual KNX.\n'area.line.device'", "invalid_ip_address": "Alamat IPv4 tidak valid", - "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah." + "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", + "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda." }, "step": { + "connection_type": { + "data": { + "connection_type": "Jenis Koneksi KNX" + }, + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "IP lokal Home Assistant", "port": "Port", + "route_back": "Dirutekan kembali/Mode NAT", "tunneling_type": "Jenis Tunnel KNX" }, "data_description": { "host": "Alamat IP perangkat tunneling KNX/IP.", "local_ip": "Kosongkan untuk menggunakan penemuan otomatis.", - "port": "Port perangkat tunneling KNX/IP." + "port": "Port perangkat tunneling KNX/IP.", + "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." }, "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." }, @@ -31,7 +42,8 @@ "individual_address": "Alamat individual", "local_ip": "IP lokal Home Assistant", "multicast_group": "Grup multicast", - "multicast_port": "Port multicast" + "multicast_port": "Port multicast", + "routing_secure": "Gunakan KNX IP Secure" }, "data_description": { "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", @@ -39,6 +51,14 @@ }, "description": "Konfigurasikan opsi routing." }, + "secure_key_source": { + "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", + "secure_routing_manual": "Konfigurasikan kunci backbone aman IP secara manual", + "secure_tunnel_manual": "Konfigurasikan kredensial aman IP secara manual" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "Nama file `.knxkeys` Anda (termasuk ekstensi)", @@ -50,7 +70,18 @@ }, "description": "Masukkan informasi untuk file `.knxkeys` Anda." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Kunci backbone", + "sync_latency_tolerance": "Toleransi latensi jaringan" + }, + "data_description": { + "backbone_key": "Dapat dilihat dalam laporan 'Security' dari proyek ETS. Mis. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Bawaannya bernilai 1000." + }, + "description": "Masukkan informasi IP aman Anda." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "Kata sandi autentikasi perangkat", "user_id": "ID pengguna", @@ -67,7 +98,7 @@ "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", - "secure_manual": "Konfigurasikan kunci aman IP secara manual" + "secure_tunnel_manual": "Konfigurasikan kunci aman IP secara manual" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "Koneksi Tunnel KNX" }, "description": "Pilih gateway dari daftar." - }, - "type": { - "data": { - "connection_type": "Jenis Koneksi KNX" - }, - "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." } } }, "options": { + "error": { + "cannot_connect": "Gagal terhubung", + "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", + "invalid_backbone_key": "Kunci backbone tidak valid. Diharapkan 32 angka heksadesimal.", + "invalid_individual_address": "Nilai tidak cocok dengan pola untuk alamat individual KNX.\n'area.line.device'", + "invalid_ip_address": "Alamat IPv4 tidak valid", + "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", + "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "Jenis Koneksi KNX", - "individual_address": "Alamat individu default", - "local_ip": "IP lokal Home Assistant", - "multicast_group": "Grup multicast", - "multicast_port": "Port multicast", "rate_limit": "Batas data", "state_updater": "Pembaruan status" }, "data_description": { - "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", - "local_ip": "Gunakan `0.0.0.0` untuk penemuan otomatis.", - "multicast_group": "Digunakan untuk perutean dan penemuan. Bawaan: `224.0.23.12`", - "multicast_port": "Digunakan untuk perutean dan penemuan. Bawaan: `3671`", - "rate_limit": "Telegram keluar maksimum per detik.\nDirekomendasikan: 20 hingga 40", + "rate_limit": "Telegram keluar maksimum per detik. `0` untuk menonaktifkan batas. Direkomendasikan: 0 atau 20 hingga 40", "state_updater": "Menyetel default untuk status pembacaan KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status entitas dari KNX Bus. Hal ini bisa ditimpa dengan opsi entitas `sync_state`." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "Jenis Koneksi KNX" + }, + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + }, + "manual_tunnel": { "data": { "host": "Host", + "local_ip": "IP lokal Home Assistant", "port": "Port", + "route_back": "Dirutekan kembali/Mode NAT", "tunneling_type": "Jenis Tunnel KNX" }, "data_description": { "host": "Alamat IP perangkat tunneling KNX/IP.", - "port": "Port perangkat tunneling KNX/IP." + "local_ip": "Kosongkan untuk menggunakan penemuan otomatis.", + "port": "Port perangkat tunneling KNX/IP.", + "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." + }, + "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." + }, + "options_init": { + "menu_options": { + "communication_settings": "Pengaturan komunikasi", + "connection_type": "Konfigurasikan antarmuka KNX" } + }, + "routing": { + "data": { + "individual_address": "Alamat individual", + "local_ip": "IP lokal Home Assistant", + "multicast_group": "Grup multicast", + "multicast_port": "Port multicast", + "routing_secure": "Gunakan KNX IP Secure" + }, + "data_description": { + "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", + "local_ip": "Kosongkan untuk menggunakan penemuan otomatis." + }, + "description": "Konfigurasikan opsi routing." + }, + "secure_key_source": { + "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", + "secure_routing_manual": "Konfigurasikan kunci backbone aman IP secara manual", + "secure_tunnel_manual": "Konfigurasikan kredensial aman IP secara manual" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nama file `.knxkeys` Anda (termasuk ekstensi)", + "knxkeys_password": "Kata sandi untuk mendekripsi file `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "File diharapkan dapat ditemukan di direktori konfigurasi Anda di `.storage/knx/`.\nDi Home Assistant OS ini akan menjadi `/config/.storage/knx/`\nContoh: `proyek_saya.knxkeys`", + "knxkeys_password": "Ini disetel saat mengekspor file dari ETS." + }, + "description": "Masukkan informasi untuk file `.knxkeys` Anda." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Kunci backbone", + "sync_latency_tolerance": "Toleransi latensi jaringan" + }, + "data_description": { + "backbone_key": "Dapat dilihat dalam laporan 'Security' dari proyek ETS. Mis. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Bawaannya bernilai 1000." + }, + "description": "Masukkan informasi IP aman Anda." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Kata sandi autentikasi perangkat", + "user_id": "ID pengguna", + "user_password": "Kata sandi pengguna" + }, + "data_description": { + "device_authentication": "Ini diatur dalam panel 'IP' dalam antarmuka di ETS.", + "user_id": "Ini sering kali merupakan tunnel nomor +1. Jadi 'Tunnel 2' akan memiliki User-ID '3'.", + "user_password": "Kata sandi untuk koneksi tunnel tertentu yang diatur di panel 'Properties' tunnel di ETS." + }, + "description": "Masukkan informasi IP aman Anda." + }, + "secure_tunneling": { + "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", + "secure_tunnel_manual": "Konfigurasikan kunci aman IP secara manual" + } + }, + "tunnel": { + "data": { + "gateway": "Koneksi Tunnel KNX" + }, + "description": "Pilih gateway dari daftar." } } } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index 8c2c58ad2d5..75c6cda6ab5 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -9,20 +9,30 @@ "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "invalid_individual_address": "Il valore non corrisponde al modello per l'indirizzo individuale KNX. 'area.line.device'", "invalid_ip_address": "Indirizzo IPv4 non valido.", - "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata." + "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", + "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipo di connessione KNX" + }, + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "IP locale di Home Assistant", "port": "Porta", + "route_back": "Modalit\u00e0 Route Back / NAT", "tunneling_type": "Tipo tunnel KNX" }, "data_description": { "host": "Indirizzo IP del dispositivo di tunneling KNX/IP.", "local_ip": "Lascia vuoto per usare il rilevamento automatico.", - "port": "Porta del dispositivo di tunneling KNX/IP." + "port": "Porta del dispositivo di tunneling KNX/IP.", + "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." }, "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." }, @@ -35,7 +45,7 @@ }, "data_description": { "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", - "local_ip": "Lasciare vuoto per usare il rilevamento automatico." + "local_ip": "Lascia vuoto per usare il rilevamento automatico." }, "description": "Configura le opzioni di instradamento." }, @@ -45,12 +55,12 @@ "knxkeys_password": "La password per decifrare il file `.knxkeys`" }, "data_description": { - "knxkeys_filename": "Il file dovrebbe essere trovato nella tua cartella di configurazione in `.storage/knx/`.\n Nel Sistema Operativo di Home Assistant questo sarebbe `/config/.storage/knx/`\n Esempio: `mio_progetto.knxkeys`", + "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." }, "description": "Inserisci le informazioni per il tuo file `.knxkeys`." }, - "secure_manual": { + "secure_tunnel_manual": { "data": { "device_authentication": "Password di autenticazione del dispositivo", "user_id": "ID utente", @@ -67,7 +77,7 @@ "description": "Seleziona come vuoi configurare KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Utilizza un file `.knxkeys` contenente chiavi di sicurezza IP", - "secure_manual": "Configura manualmente le chiavi di sicurezza IP" + "secure_tunnel_manual": "Configura manualmente le chiavi di sicurezza IP" } }, "tunnel": { @@ -75,46 +85,107 @@ "gateway": "Connessione tunnel KNX" }, "description": "Seleziona un gateway dall'elenco." - }, - "type": { - "data": { - "connection_type": "Tipo di connessione KNX" - }, - "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX.\n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo Bus KNX eseguendo una scansione del gateway.\n TUNNELING - L'integrazione si collegher\u00e0 al bus KNX tramite tunnel.\n ROUTING - L'integrazione si collegher\u00e0 al bus KNX tramite instradamento." } } }, "options": { + "error": { + "cannot_connect": "Impossibile connettersi", + "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", + "invalid_individual_address": "Il valore non corrisponde al modello per l'indirizzo individuale KNX. 'area.line.device'", + "invalid_ip_address": "Indirizzo IPv4 non valido.", + "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", + "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "Tipo di connessione KNX", - "individual_address": "Indirizzo individuale predefinito", - "local_ip": "IP locale di Home Assistant", - "multicast_group": "Gruppo multicast", - "multicast_port": "Porta multicast", - "rate_limit": "Limite di tariffa", + "rate_limit": "Limite di velocit\u00e0", "state_updater": "Aggiornatore di stato" }, "data_description": { - "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", - "local_ip": "Usa `0.0.0.0` per il rilevamento automatico.", - "multicast_group": "Utilizzato per l'instradamento e il rilevamento. Predefinito: `224.0.23.12`", - "multicast_port": "Utilizzato per l'instradamento e il rilevamento. Predefinito: `3671`", - "rate_limit": "Numero massimo di telegrammi in uscita al secondo.\n Consigliato: da 20 a 40", - "state_updater": "Impostazione predefinita per la lettura degli stati dal bus KNX. Se disabilitata Home Assistant non recuperer\u00e0 attivamente gli stati delle entit\u00e0 dal bus KNX. Pu\u00f2 essere sovrascritta dalle opzioni dell'entit\u00e0 `sync_state`." + "rate_limit": "Numero massimo di telegrammi in uscita al secondo.\n'0' per disabilitare il limite. Consigliato: 0 o da 20 a 40", + "state_updater": "Impostazione predefinita per la lettura degli stati dal bus KNX. Quando disabilitato, Home Assistant non recuperer\u00e0 attivamente gli stati delle entit\u00e0 dal bus KNX. Pu\u00f2 essere sovrascritto dalle opzioni dell'entit\u00e0 `sync_state`." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "Tipo di connessione KNX" + }, + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + }, + "manual_tunnel": { "data": { "host": "Host", + "local_ip": "IP locale di Home Assistant", "port": "Porta", + "route_back": "Modalit\u00e0 Route Back / NAT", "tunneling_type": "Tipo tunnel KNX" }, "data_description": { "host": "Indirizzo IP del dispositivo di tunneling KNX/IP.", - "port": "Porta del dispositivo di tunneling KNX/IP." + "local_ip": "Lascia vuoto per usare il rilevamento automatico.", + "port": "Porta del dispositivo di tunneling KNX/IP.", + "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." + }, + "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." + }, + "options_init": { + "menu_options": { + "communication_settings": "Impostazioni di comunicazione", + "connection_type": "Configura interfaccia KNX" } + }, + "routing": { + "data": { + "individual_address": "Indirizzo individuale", + "local_ip": "IP locale di Home Assistant", + "multicast_group": "Gruppo multicast", + "multicast_port": "Porta multicast" + }, + "data_description": { + "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", + "local_ip": "Lascia vuoto per usare il rilevamento automatico." + }, + "description": "Configura le opzioni di instradamento." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Il nome del file `.knxkeys` (inclusa l'estensione)", + "knxkeys_password": "La password per decifrare il file `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", + "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." + }, + "description": "Inserisci le informazioni per il tuo file `.knxkeys`." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Password di autenticazione del dispositivo", + "user_id": "ID utente", + "user_password": "Password utente" + }, + "data_description": { + "device_authentication": "Questo \u00e8 impostato nel pannello 'IP' dell'interfaccia in ETS.", + "user_id": "Questo \u00e8 spesso il tunnel numero +1. Quindi \"Tunnel 2\" avrebbe l'ID utente \"3\".", + "user_password": "Password per la connessione specifica del tunnel impostata nel pannello 'Propriet\u00e0' del tunnel in ETS." + }, + "description": "Inserisci le tue informazioni di sicurezza IP." + }, + "secure_tunneling": { + "description": "Seleziona come vuoi configurare KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilizza un file `.knxkeys` contenente chiavi di sicurezza IP", + "secure_tunnel_manual": "Configura manualmente le chiavi di sicurezza IP" + } + }, + "tunnel": { + "data": { + "gateway": "Connessione tunnel KNX" + }, + "description": "Seleziona un gateway dall'elenco." } } } diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 3272508a525..47af3f3998b 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -50,24 +50,10 @@ }, "description": "'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, - "secure_manual": { - "data": { - "device_authentication": "\u30c7\u30d0\u30a4\u30b9\u8a8d\u8a3c\u30d1\u30b9\u30ef\u30fc\u30c9", - "user_id": "\u30e6\u30fc\u30b6\u30fcID", - "user_password": "\u30e6\u30fc\u30b6\u30fc\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "data_description": { - "device_authentication": "\u3053\u308c\u306f\u3001ETS\u306e\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30fc\u30b9\u306e 'IP' \u30d1\u30cd\u30eb\u3067\u8a2d\u5b9a\u3057\u307e\u3059\u3002", - "user_id": "\u591a\u304f\u306e\u5834\u5408\u3001\u3053\u308c\u306f\u30c8\u30f3\u30cd\u30eb\u756a\u53f7+1\u3067\u3059\u3002\u3057\u305f\u304c\u3063\u3066\u3001 '\u30c8\u30f3\u30cd\u30eb2' \u306e\u30e6\u30fc\u30b6\u30fcID\u306f\u3001'3 '\u306b\u306a\u308a\u307e\u3059\u3002", - "user_password": "ETS\u306e\u30c8\u30f3\u30cd\u30eb\u306e\u3001'\u30d7\u30ed\u30d1\u30c6\u30a3' \u30d1\u30cd\u30eb\u3067\u8a2d\u5b9a\u3055\u308c\u305f\u7279\u5b9a\u306e\u30c8\u30f3\u30cd\u30eb\u63a5\u7d9a\u7528\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" - }, - "description": "IP \u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - }, "secure_tunneling": { "description": "KNX/IP \u30bb\u30ad\u30e5\u30a2\u3092\u69cb\u6210\u3059\u308b\u65b9\u6cd5\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "menu_options": { - "secure_knxkeys": "IP \u30bb\u30ad\u30e5\u30a2 \u30ad\u30fc\u3092\u542b\u3080\u300c.knxkeys\u300d\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3059\u308b", - "secure_manual": "IP \u30bb\u30ad\u30e5\u30a2 \u30ad\u30fc\u3092\u624b\u52d5\u3067\u69cb\u6210\u3059\u308b" + "secure_knxkeys": "IP \u30bb\u30ad\u30e5\u30a2 \u30ad\u30fc\u3092\u542b\u3080\u300c.knxkeys\u300d\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3059\u308b" } }, "tunnel": { @@ -75,46 +61,6 @@ "gateway": "KNX\u30c8\u30f3\u30cd\u30eb\u63a5\u7d9a" }, "description": "\u30ea\u30b9\u30c8\u304b\u3089\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - }, - "type": { - "data": { - "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7" - }, - "description": "KNX\u63a5\u7d9a\u306b\u4f7f\u7528\u3059\u308b\u63a5\u7d9a\u30bf\u30a4\u30d7\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n AUTOMATIC - \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u30b9\u30ad\u30e3\u30f3\u3092\u5b9f\u884c\u3057\u3066\u3001KNX \u30d0\u30b9\u3078\u306e\u63a5\u7d9a\u3092\u884c\u3044\u307e\u3059\u3002 \n TUNNELING - \u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u3092\u4ecb\u3057\u3066\u3001KNX\u30d0\u30b9\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002 \n ROUTING - \u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3092\u4ecb\u3057\u3066\u3001KNX \u30d0\u30b9\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7", - "individual_address": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", - "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", - "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", - "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8", - "rate_limit": "1 \u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u96fb\u5831(telegrams )\u6570", - "state_updater": "KNX\u30d0\u30b9\u304b\u3089\u306e\u8aad\u307f\u53d6\u308a\u72b6\u614b\u3092\u30b0\u30ed\u30fc\u30d0\u30eb\u306b\u6709\u52b9\u306b\u3059\u308b" - }, - "data_description": { - "individual_address": "Home Assistant\u304c\u4f7f\u7528\u3059\u308bKNX\u30a2\u30c9\u30ec\u30b9\u3001\u4f8b. `0.0.4`", - "local_ip": "\u81ea\u52d5\u691c\u51fa\u306b\u306f\u3001`0.0.0.0` \u3092\u4f7f\u7528\u3057\u307e\u3059\u3002", - "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8t: `224.0.23.12`", - "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8: `3671`", - "rate_limit": "1\u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u30c6\u30ec\u30b0\u30e9\u30e0\u3002\n\u63a8\u5968: 20\uff5e40", - "state_updater": "KNX \u30d0\u30b9\u304b\u3089\u72b6\u614b\u3092\u8aad\u307f\u53d6\u308b\u305f\u3081\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u7121\u52b9\u306b\u3059\u308b\u3068\u3001Home Assistant \u306f KNX \u30d0\u30b9\u304b\u3089\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u72b6\u614b\u3092\u7a4d\u6975\u7684\u306b\u53d6\u5f97\u3057\u307e\u305b\u3093\u3002 \u300csync_state\u300d\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3 \u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u3067\u304d\u307e\u3059\u3002" - } - }, - "tunnel": { - "data": { - "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8", - "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" - }, - "data_description": { - "host": "KNX/IP\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3002", - "port": "KNX/IP\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306e\u30dd\u30fc\u30c8\u3002" - } } } } diff --git a/homeassistant/components/knx/translations/ko.json b/homeassistant/components/knx/translations/ko.json index 0aac9be9341..c65ad01ff79 100644 --- a/homeassistant/components/knx/translations/ko.json +++ b/homeassistant/components/knx/translations/ko.json @@ -5,9 +5,6 @@ "data": { "knxkeys_filename": "`.knxkeys` \ud30c\uc77c\uc758 \ud30c\uc77c \uc774\ub984(\ud655\uc7a5\uc790 \ud3ec\ud568)" } - }, - "secure_manual": { - "description": "IP \ubcf4\uc548 \uc815\ubcf4\ub97c \uc785\ub825\ud558\uc138\uc694." } } } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index bc8f03eb06c..897b2dc533e 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -50,24 +50,10 @@ }, "description": "Voer de informatie voor uw `.knxkeys` bestand in." }, - "secure_manual": { - "data": { - "device_authentication": "Wachtwoord voor apparaatverificatie", - "user_id": "User ID", - "user_password": "Gebruikerswachtwoord" - }, - "data_description": { - "device_authentication": "Dit wordt ingesteld in het \"IP\"-paneel van de interface in ETS.", - "user_id": "Dit is vaak tunnelnummer +1. Dus 'Tunnel 2' zou User-ID '3' hebben.", - "user_password": "Wachtwoord voor de specifieke tunnelverbinding, ingesteld in het paneel \"Eigenschappen\" van de tunnel in ETS." - }, - "description": "Voer uw beveiligde IP-gegevens in." - }, "secure_tunneling": { "description": "Kies hoe u KNX/IP Secure wilt configureren.", "menu_options": { - "secure_knxkeys": "Gebruik een `.knxkeys` bestand met IP beveiligde sleutels", - "secure_manual": "IP-beveiligingssleutels handmatig configureren" + "secure_knxkeys": "Gebruik een `.knxkeys` bestand met IP beveiligde sleutels" } }, "tunnel": { @@ -75,46 +61,58 @@ "gateway": "KNX Tunnel Connection" }, "description": "Selecteer een gateway uit de lijst." - }, - "type": { - "data": { - "connection_type": "KNX-verbindingstype" - }, - "description": "Voer het verbindingstype in dat we moeten gebruiken voor uw KNX-verbinding.\n AUTOMATISCH - De integratie zorgt voor de connectiviteit met uw KNX-bus door een gateway-scan uit te voeren.\n TUNNELING - De integratie maakt verbinding met uw KNX-bus via tunneling.\n ROUTING - De integratie maakt via routing verbinding met uw KNX-bus." } } }, "options": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "file_not_found": "Het opgegeven `.knxkeys`-bestand is niet gevonden in het pad config/.storage/knx/", + "invalid_individual_address": "Waarde komt niet overeen met patroon voor KNX individueel adres.\n\"area.line.device" + }, "step": { - "init": { + "manual_tunnel": { + "data_description": { + "local_ip": "Leeg laten om auto-discovery te gebruiken.", + "port": "Poort van het KNX/IP-tunnelapparaat." + }, + "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in." + }, + "routing": { "data": { - "connection_type": "KNX-verbindingstype", - "individual_address": "Standaard individueel adres", + "individual_address": "Individueel adres", "local_ip": "Lokale IP van Home Assistant", "multicast_group": "Multicast-groep", - "multicast_port": "Multicast-poort", - "rate_limit": "Rate limit", - "state_updater": "Statusupdater" + "multicast_port": "Multicast-poort" }, "data_description": { - "individual_address": "KNX-adres dat door Home Assistant moet worden gebruikt, bijv. `0.0.4`", - "local_ip": "Gebruik `0.0.0.0` voor auto-discovery.", - "multicast_group": "Gebruikt voor routing en discovery. Standaard: `224.0.23.12`.", - "multicast_port": "Gebruikt voor routing en discovery. Standaard: `3671`", - "rate_limit": "Maximaal aantal uitgaande telegrammen per seconde.\nAanbevolen: 20 tot 40", - "state_updater": "Globaal in- of uitschakelen van het lezen van de status van de KNX bus. Indien uitgeschakeld, zal Home Assistant niet actief de status van de KNX Bus ophalen, `sync_state` entiteitsopties zullen geen effect hebben." + "individual_address": "KNX-adres te gebruiken door Home Assistant, bijv. `0.0.4`", + "local_ip": "Leeg laten om auto-discovery te gebruiken." + }, + "description": "Configureer de routing opties" + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "De bestandsnaam van uw `.knxkeys` bestand (inclusief extensie)", + "knxkeys_password": "Het wachtwoord om het bestand `.knxkeys` te ontcijferen" + }, + "data_description": { + "knxkeys_filename": "Het bestand zal naar verwachting worden gevonden in uw configuratiemap in '.storage/knx/'.\nIn Home Assistant OS zou dit '/config/.storage/knx/' zijn.\nVoorbeeld: 'my_project.knxkeys'", + "knxkeys_password": "Dit werd ingesteld bij het exporteren van het bestand van ETS." + }, + "description": "Voer de informatie voor uw `.knxkeys` bestand in." + }, + "secure_tunneling": { + "description": "Kies hoe u KNX/IP Secure wilt configureren.", + "menu_options": { + "secure_knxkeys": "Gebruik een `.knxkeys` bestand met IP beveiligde sleutels" } }, "tunnel": { "data": { - "host": "Host", - "port": "Poort", - "tunneling_type": "KNX Tunneling Type" + "gateway": "KNX Tunnel Connection" }, - "data_description": { - "host": "IP adres van het KNX/IP tunneling apparaat.", - "port": "Poort van het KNX/IP-tunnelapparaat." - } + "description": "Selecteer een gateway uit de lijst." } } } diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 596071695b4..8ddccdf25a3 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", + "invalid_backbone_key": "Ugyldig ryggradsn\u00f8kkel. 32 heksadesimale tall forventet.", "invalid_individual_address": "Verdien samsvarer ikke med m\u00f8nsteret for individuelle KNX-adresser.\n 'area.line.device'", "invalid_ip_address": "Ugyldig IPv4-adresse.", - "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil." + "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", + "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX tilkoblingstype" + }, + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + }, "manual_tunnel": { "data": { "host": "Vert", "local_ip": "Lokal IP for hjemmeassistent", "port": "Port", + "route_back": "Rute tilbake / NAT-modus", "tunneling_type": "KNX tunneltype" }, "data_description": { "host": "IP-adressen til KNX/IP-tunnelenheten.", "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse.", - "port": "Port p\u00e5 KNX/IP-tunnelenheten." + "port": "Port p\u00e5 KNX/IP-tunnelenheten.", + "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." }, "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." }, @@ -31,7 +42,8 @@ "individual_address": "Individuell adresse", "local_ip": "Lokal IP for hjemmeassistent", "multicast_group": "Multicast gruppe", - "multicast_port": "Multicast port" + "multicast_port": "Multicast port", + "routing_secure": "Bruk KNX IP Secure" }, "data_description": { "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", @@ -39,6 +51,14 @@ }, "description": "Vennligst konfigurer rutealternativene." }, + "secure_key_source": { + "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder sikre IP-n\u00f8kler", + "secure_routing_manual": "Konfigurer IP sikker ryggradsn\u00f8kkel manuelt", + "secure_tunnel_manual": "Konfigurer IP sikker legitimasjon manuelt" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "Filnavnet til `.knxkeys`-filen (inkludert utvidelse)", @@ -50,11 +70,22 @@ }, "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen." }, - "secure_manual": { + "secure_routing_manual": { "data": { - "device_authentication": "Passord for enhetsgodkjenning", + "backbone_key": "Ryggraden n\u00f8kkel", + "sync_latency_tolerance": "Toleranse for nettverksventetid" + }, + "data_description": { + "backbone_key": "Kan sees i 'Sikkerhet'-rapporten til et ETS-prosjekt. F.eks. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Standard er 1000." + }, + "description": "Vennligst skriv inn din sikre IP-informasjon." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Enhetsautentiseringspassord", "user_id": "bruker-ID", - "user_password": "Brukerpassord" + "user_password": "Bruker passord" }, "data_description": { "device_authentication": "Dette settes i 'IP'-panelet til grensesnittet i ETS.", @@ -67,7 +98,7 @@ "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder IP-sikre n\u00f8kler", - "secure_manual": "Konfigurer IP-sikre n\u00f8kler manuelt" + "secure_tunnel_manual": "Konfigurer IP-sikre n\u00f8kler manuelt" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "KNX Tunneltilkobling" }, "description": "Vennligst velg en gateway fra listen." - }, - "type": { - "data": { - "connection_type": "KNX tilkoblingstype" - }, - "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til KNX-bussen din via tunnelering.\n ROUTING - Integrasjonen vil kobles til din KNX-bussen via ruting." } } }, "options": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", + "invalid_backbone_key": "Ugyldig ryggradsn\u00f8kkel. 32 heksadesimale tall forventet.", + "invalid_individual_address": "Verdien samsvarer ikke med m\u00f8nsteret for individuelle KNX-adresser.\n 'area.line.device'", + "invalid_ip_address": "Ugyldig IPv4-adresse.", + "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", + "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "KNX tilkoblingstype", - "individual_address": "Standard individuell adresse", - "local_ip": "Lokal IP for hjemmeassistent", - "multicast_group": "Multicast gruppe", - "multicast_port": "Multicast port", "rate_limit": "Satsgrense", "state_updater": "Statens oppdatering" }, "data_description": { - "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", - "local_ip": "Bruk `0.0.0.0` for automatisk oppdagelse.", - "multicast_group": "Brukes til ruting og oppdagelse. Standard: `224.0.23.12`", - "multicast_port": "Brukes til ruting og oppdagelse. Standard: `3671`", - "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund.\n Anbefalt: 20 til 40", - "state_updater": "Sett standard for lesing av tilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente enhetstilstander fra KNX-bussen. Kan overstyres av entitetsalternativer for \"sync_state\"." + "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund.\n `0` for \u00e5 deaktivere grensen. Anbefalt: 0 eller 20 til 40", + "state_updater": "Angi standard for lesing av tilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente enhetstilstander fra KNX-bussen. Kan overstyres av entitetsalternativer for \"sync_state\"." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "KNX tilkoblingstype" + }, + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + }, + "manual_tunnel": { "data": { "host": "Vert", + "local_ip": "Lokal IP for hjemmeassistent", "port": "Port", + "route_back": "Rute tilbake / NAT-modus", "tunneling_type": "KNX tunneltype" }, "data_description": { "host": "IP-adressen til KNX/IP-tunnelenheten.", - "port": "Port p\u00e5 KNX/IP-tunnelenheten." + "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse.", + "port": "Port p\u00e5 KNX/IP-tunnelenheten.", + "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." + }, + "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." + }, + "options_init": { + "menu_options": { + "communication_settings": "Kommunikasjonsinnstillinger", + "connection_type": "Konfigurer KNX-grensesnitt" } + }, + "routing": { + "data": { + "individual_address": "Individuell adresse", + "local_ip": "Lokal IP for hjemmeassistent", + "multicast_group": "Multicast gruppe", + "multicast_port": "Multicast port", + "routing_secure": "Bruk KNX IP Secure" + }, + "data_description": { + "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", + "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse." + }, + "description": "Vennligst konfigurer rutealternativene." + }, + "secure_key_source": { + "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder sikre IP-n\u00f8kler", + "secure_routing_manual": "Konfigurer IP sikker ryggradsn\u00f8kkel manuelt", + "secure_tunnel_manual": "Konfigurer IP sikker legitimasjon manuelt" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Filnavnet til `.knxkeys`-filen (inkludert utvidelse)", + "knxkeys_password": "Passordet for \u00e5 dekryptere `.knxkeys`-filen" + }, + "data_description": { + "knxkeys_filename": "Filen forventes \u00e5 bli funnet i konfigurasjonskatalogen din i `.storage/knx/`.\n I Home Assistant OS vil dette v\u00e6re `/config/.storage/knx/`\n Eksempel: `mitt_prosjekt.knxkeys`", + "knxkeys_password": "Dette ble satt ved eksport av filen fra ETS." + }, + "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Ryggraden n\u00f8kkel", + "sync_latency_tolerance": "Toleranse for nettverksventetid" + }, + "data_description": { + "backbone_key": "Kan sees i 'Sikkerhet'-rapporten til et ETS-prosjekt. F.eks. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Standard er 1000." + }, + "description": "Vennligst skriv inn din sikre IP-informasjon." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Enhetsautentiseringspassord", + "user_id": "bruker-ID", + "user_password": "Bruker passord" + }, + "data_description": { + "device_authentication": "Dette settes i 'IP'-panelet til grensesnittet i ETS.", + "user_id": "Dette er ofte tunnelnummer +1. S\u00e5 'Tunnel 2' ville ha bruker-ID '3'.", + "user_password": "Passord for den spesifikke tunnelforbindelsen satt i 'Egenskaper'-panelet i tunnelen i ETS." + }, + "description": "Vennligst skriv inn din sikre IP-informasjon." + }, + "secure_tunneling": { + "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder IP-sikre n\u00f8kler", + "secure_tunnel_manual": "Konfigurer IP-sikre n\u00f8kler manuelt" + } + }, + "tunnel": { + "data": { + "gateway": "KNX Tunneltilkobling" + }, + "description": "Vennligst velg en gateway fra listen." } } } diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index 23185a0ae49..98a9e968595 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -9,20 +9,30 @@ "file_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", "invalid_individual_address": "Warto\u015b\u0107 nie pasuje do wzorca dla indywidualnego adresu KNX.\n 'obszar.linia.urz\u0105dzenie'", "invalid_ip_address": "Nieprawid\u0142owy adres IPv4.", - "invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe." + "invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "no_router_discovered": "Nie wykryto w sieci routera KNXnet/IP.", + "no_tunnel_discovered": "Nie mo\u017cna znale\u017a\u0107 serwera tuneluj\u0105cego KNX w Twojej sieci." }, "step": { + "connection_type": { + "data": { + "connection_type": "Typ po\u0142\u0105czenia KNX" + }, + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + }, "manual_tunnel": { "data": { "host": "Nazwa hosta lub adres IP", "local_ip": "Lokalny adres IP Home Assistanta", "port": "Port", + "route_back": "Tryb Route Back / NAT", "tunneling_type": "Typ tunelowania KNX" }, "data_description": { "host": "Adres IP urz\u0105dzenia tuneluj\u0105cego KNX/IP.", "local_ip": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania.", - "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP." + "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP.", + "route_back": "W\u0142\u0105cz, je\u015bli serwer tuneluj\u0105cy KNXnet/IP znajduje si\u0119 za NAT. Dotyczy tylko po\u0142\u0105cze\u0144 UDP." }, "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." }, @@ -50,7 +60,7 @@ }, "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`." }, - "secure_manual": { + "secure_tunnel_manual": { "data": { "device_authentication": "Has\u0142o uwierzytelniania urz\u0105dzenia", "user_id": "Identyfikator u\u017cytkownika", @@ -67,7 +77,7 @@ "description": "Wybierz, jak chcesz skonfigurowa\u0107 KNX/IP secure.", "menu_options": { "secure_knxkeys": "U\u017cyj pliku `.knxkeys` zawieraj\u0105cego klucze IP secure", - "secure_manual": "R\u0119czna konfiguracja kluczy IP secure" + "secure_tunnel_manual": "R\u0119czna konfiguracja kluczy IP secure" } }, "tunnel": { @@ -75,46 +85,107 @@ "gateway": "Po\u0142\u0105czenie tunelowe KNX" }, "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy." - }, - "type": { - "data": { - "connection_type": "Typ po\u0142\u0105czenia KNX" - }, - "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \n AUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \n TUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \n ROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." } } }, "options": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "file_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", + "invalid_individual_address": "Warto\u015b\u0107 nie pasuje do wzorca dla indywidualnego adresu KNX.\n 'obszar.linia.urz\u0105dzenie'", + "invalid_ip_address": "Nieprawid\u0142owy adres IPv4.", + "invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "no_router_discovered": "Nie wykryto w sieci routera KNXnet/IP.", + "no_tunnel_discovered": "Nie mo\u017cna znale\u017a\u0107 serwera tuneluj\u0105cego KNX w Twojej sieci." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "Typ po\u0142\u0105czenia KNX", - "individual_address": "Domy\u015blny adres indywidualny", - "local_ip": "Lokalny adres IP Home Assistanta", - "multicast_group": "Grupa multicast", - "multicast_port": "Port multicast", "rate_limit": "Limit", "state_updater": "Aktualizator stanu" }, "data_description": { - "individual_address": "Adres KNX u\u017cywany przez Home Assistanta, np. `0.0.4`", - "local_ip": "U\u017cyj `0.0.0.0` do automatycznego wykrywania.", - "multicast_group": "U\u017cywany do routingu i wykrywania. Domy\u015blnie: `224.0.23.12`", - "multicast_port": "U\u017cywany do routingu i wykrywania. Domy\u015blnie: `3671`", - "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119.\nZalecane: od 20 do 40", + "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119.\n \u201e0\u201d, aby wy\u0142\u0105czy\u0107 limit. Zalecane: 0 lub 20 do 40", "state_updater": "Ustaw domy\u015blne odczytywanie stan\u00f3w z magistrali KNX. Po wy\u0142\u0105czeniu, Home Assistant nie b\u0119dzie aktywnie pobiera\u0107 stan\u00f3w encji z magistrali KNX. Mo\u017cna to zast\u0105pi\u0107 przez opcj\u0119 encji `sync_state`." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "Typ po\u0142\u0105czenia KNX" + }, + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + }, + "manual_tunnel": { "data": { "host": "Nazwa hosta lub adres IP", + "local_ip": "Lokalny adres IP Home Assistanta", "port": "Port", + "route_back": "Tryb Route Back / NAT", "tunneling_type": "Typ tunelowania KNX" }, "data_description": { "host": "Adres IP urz\u0105dzenia tuneluj\u0105cego KNX/IP.", - "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP." + "local_ip": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania.", + "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP.", + "route_back": "W\u0142\u0105cz, je\u015bli serwer tuneluj\u0105cy KNXnet/IP znajduje si\u0119 za NAT. Dotyczy tylko po\u0142\u0105cze\u0144 UDP." + }, + "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." + }, + "options_init": { + "menu_options": { + "communication_settings": "Ustawienia komunikacji", + "connection_type": "Konfiguracja interfejsu KNX" } + }, + "routing": { + "data": { + "individual_address": "Adres indywidualny", + "local_ip": "Lokalny adres IP Home Assistanta", + "multicast_group": "Grupa multicast", + "multicast_port": "Port multicast" + }, + "data_description": { + "individual_address": "Adres KNX u\u017cywany przez Home Assistanta, np. `0.0.4`", + "local_ip": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania." + }, + "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nazwa pliku `.knxkeys` (wraz z rozszerzeniem)", + "knxkeys_password": "Has\u0142o do odszyfrowania pliku `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Plik powinien znajdowa\u0107 si\u0119 w katalogu konfiguracyjnym w `.storage/knx/`.\nW systemie Home Assistant OS b\u0119dzie to `/config/.storage/knx/`\nPrzyk\u0142ad: `m\u00f3j_projekt.knxkeys`", + "knxkeys_password": "Zosta\u0142o to ustawione podczas eksportowania pliku z ETS." + }, + "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Has\u0142o uwierzytelniania urz\u0105dzenia", + "user_id": "Identyfikator u\u017cytkownika", + "user_password": "Has\u0142o u\u017cytkownika" + }, + "data_description": { + "device_authentication": "Jest to ustawiane w panelu \u201eIP\u201d interfejsu w ETS.", + "user_id": "Cz\u0119sto jest to numer tunelu plus 1. Tak wi\u0119c \u201eTunnel 2\u201d mia\u0142by identyfikator u\u017cytkownika \u201e3\u201d.", + "user_password": "Has\u0142o dla konkretnego po\u0142\u0105czenia tunelowego ustawione w panelu \u201eW\u0142a\u015bciwo\u015bci\u201d tunelu w ETS." + }, + "description": "Wprowad\u017a informacje o IP secure." + }, + "secure_tunneling": { + "description": "Wybierz, jak chcesz skonfigurowa\u0107 KNX/IP secure.", + "menu_options": { + "secure_knxkeys": "U\u017cyj pliku `.knxkeys` zawieraj\u0105cego klucze IP secure", + "secure_tunnel_manual": "R\u0119czna konfiguracja kluczy IP secure" + } + }, + "tunnel": { + "data": { + "gateway": "Po\u0142\u0105czenie tunelowe KNX" + }, + "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy." } } } diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index dcdf057493b..5217c4315ea 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "Falha ao conectar", "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", + "invalid_backbone_key": "Chave de backbone inv\u00e1lida. 32 n\u00fameros hexadecimais esperados.", "invalid_individual_address": "O valor n\u00e3o corresponde ao padr\u00e3o do endere\u00e7o individual KNX.\n '\u00e1rea.linha.dispositivo'", "invalid_ip_address": "Endere\u00e7o IPv4 inv\u00e1lido.", - "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada." + "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", + "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipo de conex\u00e3o KNX" + }, + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + }, "manual_tunnel": { "data": { "host": "Nome do host", "local_ip": "IP local do Home Assistant", "port": "Porta", + "route_back": "Rota de volta / modo NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { "host": "Endere\u00e7o IP do dispositivo de tunelamento KNX/IP.", "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica.", - "port": "Porta do dispositivo de tunelamento KNX/IP." + "port": "Porta do dispositivo de tunelamento KNX/IP.", + "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." }, "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." }, @@ -31,7 +42,8 @@ "individual_address": "Endere\u00e7o individual", "local_ip": "IP local do Home Assistant", "multicast_group": "Grupo multicast", - "multicast_port": "Porta multicast" + "multicast_port": "Porta multicast", + "routing_secure": "Usar KNX IP Secure" }, "data_description": { "individual_address": "Endere\u00e7o KNX a ser usado pelo Home Assistant, por exemplo, `0.0.4`", @@ -39,6 +51,14 @@ }, "description": "Por favor, configure as op\u00e7\u00f5es de roteamento." }, + "secure_key_source": { + "description": "Selecione como deseja configurar o KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves IP seguras", + "secure_routing_manual": "Configure a chave de backbone IP segura manualmente", + "secure_tunnel_manual": "Configurar credenciais seguras de IP manualmente" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "O nome do seu arquivo `.knxkeys` (incluindo extens\u00e3o)", @@ -50,7 +70,18 @@ }, "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Chave de backbone", + "sync_latency_tolerance": "Toler\u00e2ncia de lat\u00eancia de rede" + }, + "data_description": { + "backbone_key": "Pode ser visto no relat\u00f3rio 'Seguran\u00e7a' de um projeto ETS. Por exemplo. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "O padr\u00e3o \u00e9 1000." + }, + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "Senha de autentica\u00e7\u00e3o do dispositivo", "user_id": "ID do usu\u00e1rio", @@ -67,7 +98,7 @@ "description": "Selecione como deseja configurar o KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves seguras de IP", - "secure_manual": "Configurar manualmente as chaves de seguran\u00e7a IP" + "secure_tunnel_manual": "Configurar chaves seguras de IP manualmente" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "Conex\u00e3o do t\u00fanel KNX" }, "description": "Selecione um gateway na lista." - }, - "type": { - "data": { - "connection_type": "Tipo de conex\u00e3o KNX" - }, - "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade ao seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." } } }, "options": { + "error": { + "cannot_connect": "Falha ao conectar", + "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", + "invalid_backbone_key": "Chave de backbone inv\u00e1lida. 32 n\u00fameros hexadecimais esperados.", + "invalid_individual_address": "O valor n\u00e3o corresponde ao padr\u00e3o do endere\u00e7o individual KNX.\n '\u00e1rea.linha.dispositivo'", + "invalid_ip_address": "Endere\u00e7o IPv4 inv\u00e1lido.", + "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", + "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "Tipo de conex\u00e3o KNX", - "individual_address": "Endere\u00e7o individual padr\u00e3o", - "local_ip": "IP local do Home Assistant", - "multicast_group": "Grupo multicast", - "multicast_port": "Porta multicast", "rate_limit": "Taxa limite", "state_updater": "Atualizador de estado" }, "data_description": { - "individual_address": "Endere\u00e7o KNX a ser usado pelo Home Assistant, por exemplo, `0.0.4`", - "local_ip": "Use `0.0.0.0` para descoberta autom\u00e1tica.", - "multicast_group": "Usado para roteamento e descoberta. Padr\u00e3o: `224.0.23.12`", - "multicast_port": "Usado para roteamento e descoberta. Padr\u00e3o: `3671`", - "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo.\n Recomendado: 20 a 40", + "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo.\n `0` para desabilitar o limite. Recomendado: 0 ou 20 a 40", "state_updater": "Defina o padr\u00e3o para estados de leitura do barramento KNX. Quando desativado, o Home Assistant n\u00e3o recuperar\u00e1 ativamente os estados de entidade do barramento KNX. Pode ser substitu\u00eddo pelas op\u00e7\u00f5es de entidade `sync_state`." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "Tipo de conex\u00e3o KNX" + }, + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + }, + "manual_tunnel": { "data": { "host": "Nome do host", + "local_ip": "IP local do Home Assistant", "port": "Porta", + "route_back": "Rota de volta / modo NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { "host": "Endere\u00e7o IP do dispositivo de tunelamento KNX/IP.", - "port": "Porta do dispositivo de tunelamento KNX/IP." + "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica.", + "port": "Porta do dispositivo de tunelamento KNX/IP.", + "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." + }, + "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." + }, + "options_init": { + "menu_options": { + "communication_settings": "Configura\u00e7\u00f5es de comunica\u00e7\u00e3o", + "connection_type": "Configurar interface KNX" } + }, + "routing": { + "data": { + "individual_address": "Endere\u00e7o individual", + "local_ip": "IP local do Home Assistant", + "multicast_group": "Grupo multicast", + "multicast_port": "Porta multicast", + "routing_secure": "Usar KNX IP Secure" + }, + "data_description": { + "individual_address": "Endere\u00e7o KNX a ser usado pelo Home Assistant, por exemplo, `0.0.4`", + "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica." + }, + "description": "Por favor, configure as op\u00e7\u00f5es de roteamento." + }, + "secure_key_source": { + "description": "Selecione como deseja configurar o KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves IP seguras", + "secure_routing_manual": "Configure a chave de backbone IP segura manualmente", + "secure_tunnel_manual": "Configurar credenciais seguras de IP manualmente" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "O nome do seu arquivo `.knxkeys` (incluindo extens\u00e3o)", + "knxkeys_password": "A senha para descriptografar o arquivo `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Espera-se que o arquivo seja encontrado em seu diret\u00f3rio de configura\u00e7\u00e3o em `.storage/knx/`.\n No sistema operacional Home Assistant seria `/config/.storage/knx/`\n Exemplo: `my_project.knxkeys`", + "knxkeys_password": "Isso foi definido ao exportar o arquivo do ETS." + }, + "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Chave de backbone", + "sync_latency_tolerance": "Toler\u00e2ncia de lat\u00eancia de rede" + }, + "data_description": { + "backbone_key": "Pode ser visto no relat\u00f3rio 'Seguran\u00e7a' de um projeto ETS. Por exemplo. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "O padr\u00e3o \u00e9 1000." + }, + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Senha de autentica\u00e7\u00e3o do dispositivo", + "user_id": "ID do usu\u00e1rio", + "user_password": "Senha do usu\u00e1rio" + }, + "data_description": { + "device_authentication": "Isso \u00e9 definido no painel 'IP' da interface no ETS.", + "user_id": "Isso geralmente \u00e9 o n\u00famero do t\u00fanel +1. Portanto, 'T\u00fanel 2' teria o ID de usu\u00e1rio '3'.", + "user_password": "Senha para a conex\u00e3o de t\u00fanel espec\u00edfica definida no painel 'Propriedades' do t\u00fanel no ETS." + }, + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + }, + "secure_tunneling": { + "description": "Selecione como deseja configurar o KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves seguras de IP", + "secure_tunnel_manual": "Configurar chaves seguras de IP manualmente" + } + }, + "tunnel": { + "data": { + "gateway": "Conex\u00e3o do t\u00fanel KNX" + }, + "description": "Selecione um gateway na lista." } } } diff --git a/homeassistant/components/knx/translations/pt.json b/homeassistant/components/knx/translations/pt.json deleted file mode 100644 index 7220ef495c9..00000000000 --- a/homeassistant/components/knx/translations/pt.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "options": { - "step": { - "tunnel": { - "data": { - "host": "Anfitri\u00e3o", - "port": "Porta" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index d5c5bef24fa..a8c9ba00ed2 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "file_not_found": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 config/.storage/knx/", + "invalid_backbone_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 backbone. \u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0445 \u0447\u0438\u0441\u043b\u0430.", "invalid_individual_address": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d\u0443 \u0434\u043b\u044f \u0438\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 KNX 'area.line.device'.", "invalid_ip_address": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 IPv4.", - "invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`." + "invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`.", + "no_router_discovered": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440 KNXnet/IP.", + "no_tunnel_discovered": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438." }, "step": { + "connection_type": { + "data": { + "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." + }, "manual_tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", "port": "\u041f\u043e\u0440\u0442", + "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT", "tunneling_type": "\u0422\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX" }, "data_description": { "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", "local_ip": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435.", - "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP." + "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", + "route_back": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNXnet/IP \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0437\u0430 NAT. \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 UDP." }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438." }, @@ -31,7 +42,8 @@ "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441", "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", - "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438" + "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", + "routing_secure": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c KNX IP Secure" }, "data_description": { "individual_address": "\u0410\u0434\u0440\u0435\u0441 KNX, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Home Assistant, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `0.0.4`", @@ -39,6 +51,14 @@ }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438." }, + "secure_key_source": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b `.knxkeys`, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u043b\u044e\u0447\u0438 IP secure", + "secure_routing_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c backbone-\u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e", + "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "\u0418\u043c\u044f \u0444\u0430\u0439\u043b\u0430 `.knxkeys` (\u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435)", @@ -50,7 +70,18 @@ }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0444\u0430\u0439\u043b\u0435 `.knxkeys`." }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "\u041a\u043b\u044e\u0447 backbone", + "sync_latency_tolerance": "\u0414\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0430\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u0441\u0435\u0442\u0438" + }, + "data_description": { + "backbone_key": "\u041c\u043e\u0436\u043d\u043e \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0432 \u043e\u0442\u0447\u0435\u0442\u0435 'Security' \u043f\u0440\u043e\u0435\u043a\u0442\u0430 ETS. Eg. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e - 1000." + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + }, + "secure_tunnel_manual": { "data": { "device_authentication": "\u041f\u0430\u0440\u043e\u043b\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "user_id": "ID \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", @@ -67,7 +98,7 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX/IP Secure.", "menu_options": { "secure_knxkeys": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b `.knxkeys`, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u043b\u044e\u0447\u0438 IP secure", - "secure_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f KNX" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430." - }, - "type": { - "data": { - "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." } } }, "options": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "file_not_found": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 config/.storage/knx/", + "invalid_backbone_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 backbone. \u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0445 \u0447\u0438\u0441\u043b\u0430.", + "invalid_individual_address": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d\u0443 \u0434\u043b\u044f \u0438\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 KNX 'area.line.device'.", + "invalid_ip_address": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 IPv4.", + "invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`.", + "no_router_discovered": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440 KNXnet/IP.", + "no_tunnel_discovered": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX", - "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", - "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", - "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", - "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", "rate_limit": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438", "state_updater": "\u0421\u0440\u0435\u0434\u0441\u0442\u0432\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f" }, "data_description": { - "individual_address": "\u0410\u0434\u0440\u0435\u0441 KNX, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Home Assistant, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `0.0.4`", - "local_ip": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 `0.0.0.0` \u0434\u043b\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.", - "multicast_group": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `224.0.23.12`", - "multicast_port": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `3671`", - "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.\n\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f: \u043e\u0442 20 \u0434\u043e 40", + "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.\n\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435: 0 \u0438\u043b\u0438 \u043e\u0442 20 \u0434\u043e 40 (`0` - \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435).", "state_updater": "\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441 \u0448\u0438\u043d\u044b KNX. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043c\u0435\u043d\u0435\u043d \u043e\u043f\u0446\u0438\u044f\u043c\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 `sync_state`." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." + }, + "manual_tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", "port": "\u041f\u043e\u0440\u0442", + "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT", "tunneling_type": "\u0422\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX" }, "data_description": { "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", - "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP." + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435.", + "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", + "route_back": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNXnet/IP \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0437\u0430 NAT. \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 UDP." + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438." + }, + "options_init": { + "menu_options": { + "communication_settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0432\u044f\u0437\u0438", + "connection_type": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 KNX" } + }, + "routing": { + "data": { + "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", + "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", + "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", + "routing_secure": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c KNX IP Secure" + }, + "data_description": { + "individual_address": "\u0410\u0434\u0440\u0435\u0441 KNX, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Home Assistant, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `0.0.4`", + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435." + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438." + }, + "secure_key_source": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b `.knxkeys`, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u043b\u044e\u0447\u0438 IP secure", + "secure_routing_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c backbone-\u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e", + "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "\u0418\u043c\u044f \u0444\u0430\u0439\u043b\u0430 `.knxkeys` (\u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435)", + "knxkeys_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u0444\u0430\u0439\u043b \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 `.storage/knx/`.\n\u0415\u0441\u043b\u0438 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 Home Assistant OS \u044d\u0442\u043e\u0442 \u043f\u0443\u0442\u044c \u0431\u0443\u0434\u0435\u0442 `/config/.storage/knx/`\n\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: `my_project.knxkeys`", + "knxkeys_password": "\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0431\u044b\u043b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043f\u0440\u0438 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0435 \u0444\u0430\u0439\u043b\u0430 \u0438\u0437 ETS." + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0444\u0430\u0439\u043b\u0435 `.knxkeys`." + }, + "secure_routing_manual": { + "data": { + "backbone_key": "\u041a\u043b\u044e\u0447 backbone", + "sync_latency_tolerance": "\u0414\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0430\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u0441\u0435\u0442\u0438" + }, + "data_description": { + "backbone_key": "\u041c\u043e\u0436\u043d\u043e \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0432 \u043e\u0442\u0447\u0435\u0442\u0435 'Security' \u043f\u0440\u043e\u0435\u043a\u0442\u0430 ETS. Eg. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e - 1000." + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "\u041f\u0430\u0440\u043e\u043b\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "user_id": "ID \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "user_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "data_description": { + "device_authentication": "\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 'IP' \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 ETS.", + "user_id": "\u0427\u0430\u0441\u0442\u043e \u043d\u043e\u043c\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u044f +1. \u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, 'Tunnel 2' \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u0435\u0442\u044c User-ID '3'.", + "user_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 'Properties' \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0432 ETS." + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + }, + "secure_tunneling": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b `.knxkeys`, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u043b\u044e\u0447\u0438 IP secure", + "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + } + }, + "tunnel": { + "data": { + "gateway": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f KNX" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430." } } } diff --git a/homeassistant/components/knx/translations/sk.json b/homeassistant/components/knx/translations/sk.json index 347dbdfbcef..b1fa9902a2f 100644 --- a/homeassistant/components/knx/translations/sk.json +++ b/homeassistant/components/knx/translations/sk.json @@ -1,32 +1,134 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_ip_address": "Neplatn\u00e1 adresa IPv4." }, "step": { + "connection_type": { + "data": { + "connection_type": "Typ pripojenia KNX" + } + }, "manual_tunnel": { "data": { + "host": "Hostite\u013e", + "local_ip": "Lok\u00e1lna IP adresa Home Assistant-a", "port": "Port" } }, - "secure_manual": { + "routing": { "data": { - "device_authentication": "Heslo na overenie zariadenia" + "individual_address": "Individu\u00e1lna adresa", + "local_ip": "Lok\u00e1lna IP adresa Home Assistant-a" + }, + "data_description": { + "individual_address": "Adresa KNX, ktor\u00fa bude pou\u017e\u00edva\u0165 Home Assistant, napr. `0.0.4`", + "local_ip": "Ak chcete pou\u017ei\u0165 automatick\u00e9 zis\u0165ovanie, nechajte pole pr\u00e1zdne." + }, + "description": "Nakonfigurujte mo\u017enosti smerovania." + }, + "secure_key_source": { + "description": "Vyberte, ako chcete nakonfigurova\u0165 KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Pou\u017eite s\u00fabor `.knxkeys` obsahuj\u00faci bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de IP", + "secure_routing_manual": "Nakonfigurujte zabezpe\u010den\u00fd k\u013e\u00fa\u010d chrbticovej siete IP manu\u00e1lne", + "secure_tunnel_manual": "Manu\u00e1lne nakonfigurujte bezpe\u010dnostn\u00e9 poverenia IP" } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "N\u00e1zov s\u00faboru v\u00e1\u0161ho s\u00faboru `.knxkeys` (vr\u00e1tane pr\u00edpony)", + "knxkeys_password": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys`" + } + }, + "secure_routing_manual": { + "data_description": { + "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." + }, + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Heslo na overenie zariadenia", + "user_id": "ID pou\u017e\u00edvate\u013ea", + "user_password": "Pou\u017e\u00edvate\u013esk\u00e9 heslo" + }, + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." } } }, "options": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_ip_address": "Neplatn\u00e1 adresa IPv4." + }, "step": { - "init": { - "data": { - "local_ip": "Lok\u00e1lna IP adresa Home Assistant-a" + "communication_settings": { + "data_description": { + "rate_limit": "Maxim\u00e1lny po\u010det odch\u00e1dzaj\u00facich telegramov za sekundu.\n `0` pre deaktiv\u00e1ciu limitu. Odpor\u00fa\u010dan\u00e9: 0 alebo 20 a\u017e 40" } }, - "tunnel": { + "connection_type": { "data": { + "connection_type": "Typ pripojenia KNX" + } + }, + "manual_tunnel": { + "data": { + "host": "Hostite\u013e", + "local_ip": "Lok\u00e1lna IP adresa Home Assistant-a", "port": "Port" } + }, + "options_init": { + "menu_options": { + "communication_settings": "Nastavenia komunik\u00e1cie", + "connection_type": "Konfigur\u00e1cia rozhrania KNX" + } + }, + "routing": { + "data": { + "individual_address": "Individu\u00e1lna adresa", + "local_ip": "Lok\u00e1lna IP adresa Home Assistant-a" + }, + "data_description": { + "individual_address": "Adresa KNX, ktor\u00fa bude pou\u017e\u00edva\u0165 Home Assistant, napr. `0.0.4`", + "local_ip": "Ak chcete pou\u017ei\u0165 automatick\u00e9 zis\u0165ovanie, nechajte pole pr\u00e1zdne." + }, + "description": "Nakonfigurujte mo\u017enosti smerovania." + }, + "secure_key_source": { + "description": "Vyberte, ako chcete nakonfigurova\u0165 KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Pou\u017eite s\u00fabor `.knxkeys` obsahuj\u00faci bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de IP", + "secure_routing_manual": "Nakonfigurujte zabezpe\u010den\u00fd k\u013e\u00fa\u010d chrbticovej siete IP manu\u00e1lne", + "secure_tunnel_manual": "Manu\u00e1lne nakonfigurujte bezpe\u010dnostn\u00e9 poverenia IP" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "N\u00e1zov s\u00faboru v\u00e1\u0161ho s\u00faboru `.knxkeys` (vr\u00e1tane pr\u00edpony)", + "knxkeys_password": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys`" + } + }, + "secure_routing_manual": { + "data_description": { + "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." + }, + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Heslo na overenie zariadenia", + "user_id": "ID pou\u017e\u00edvate\u013ea", + "user_password": "Pou\u017e\u00edvate\u013esk\u00e9 heslo" + }, + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." } } } diff --git a/homeassistant/components/knx/translations/sl.json b/homeassistant/components/knx/translations/sl.json index d44b7dbd9cb..6d4ffbc590c 100644 --- a/homeassistant/components/knx/translations/sl.json +++ b/homeassistant/components/knx/translations/sl.json @@ -24,20 +24,5 @@ "description": "Prosimo, izberite prehod s seznama." } } - }, - "options": { - "step": { - "init": { - "data": { - "individual_address": "Privzet individualni naslov" - } - }, - "tunnel": { - "data": { - "host": "Gostitelj", - "port": "Vrata" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 8abd3264e18..9ee17f9dec2 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -50,24 +50,10 @@ }, "description": "V\u00e4nligen ange informationen f\u00f6r din `.knxkeys`-fil." }, - "secure_manual": { - "data": { - "device_authentication": "L\u00f6senord f\u00f6r enhetsautentisering", - "user_id": "Anv\u00e4ndar-ID", - "user_password": "Anv\u00e4ndarl\u00f6senord" - }, - "data_description": { - "device_authentication": "Detta st\u00e4lls in i 'IP'-panelen i gr\u00e4nssnittet i ETS.", - "user_id": "Detta \u00e4r ofta tunnelnummer +1. S\u00e5 'Tunnel 2' skulle ha anv\u00e4ndar-ID '3'.", - "user_password": "L\u00f6senord f\u00f6r den specifika tunnelanslutningen som anges i panelen \"Egenskaper\" i tunneln i ETS." - }, - "description": "Ange din s\u00e4kra IP-information." - }, "secure_tunneling": { "description": "V\u00e4lj hur du vill konfigurera KNX/IP Secure.", "menu_options": { - "secure_knxkeys": "Anv\u00e4nd en fil `.knxkeys` som inneh\u00e5ller s\u00e4kra IP-nycklar.", - "secure_manual": "Konfigurera s\u00e4kra IP nycklar manuellt" + "secure_knxkeys": "Anv\u00e4nd en fil `.knxkeys` som inneh\u00e5ller s\u00e4kra IP-nycklar." } }, "tunnel": { @@ -75,46 +61,6 @@ "gateway": "KNX-tunnelanslutning" }, "description": "V\u00e4lj en gateway fr\u00e5n listan." - }, - "type": { - "data": { - "connection_type": "KNX anslutningstyp" - }, - "description": "Ange vilken anslutningstyp vi ska anv\u00e4nda f\u00f6r din KNX-anslutning.\n AUTOMATISK - Integrationen tar hand om anslutningen till din KNX Bus genom att utf\u00f6ra en gateway-skanning.\n TUNNELING - Integrationen kommer att ansluta till din KNX-buss via tunnling.\n ROUTING - Integrationen kommer att ansluta till din KNX-buss via routing." - } - } - }, - "options": { - "step": { - "init": { - "data": { - "connection_type": "KNX anslutningstyp", - "individual_address": "Enskild standardadress", - "local_ip": "Lokal IP f\u00f6r Home Assistant", - "multicast_group": "Multicast-grupp", - "multicast_port": "Multicast-port", - "rate_limit": "Hastighetsgr\u00e4ns", - "state_updater": "Tillst\u00e5ndsuppdaterare" - }, - "data_description": { - "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", - "local_ip": "Anv\u00e4nd `0.0.0.0.0` f\u00f6r automatisk identifiering.", - "multicast_group": "Anv\u00e4nds f\u00f6r routing och uppt\u00e4ckt. Standard: \"224.0.23.12\".", - "multicast_port": "Anv\u00e4nds f\u00f6r routing och uppt\u00e4ckt. Standard: \"3671\".", - "rate_limit": "Maximalt antal utg\u00e5ende telegram per sekund.\n Rekommenderad: 20 till 40", - "state_updater": "St\u00e4ll in som standard f\u00f6r att l\u00e4sa tillst\u00e5nd fr\u00e5n KNX-bussen. N\u00e4r den \u00e4r inaktiverad kommer Home Assistant inte aktivt att h\u00e4mta entitetstillst\u00e5nd fr\u00e5n KNX-bussen. Kan \u00e5sidos\u00e4ttas av entitetsalternativ \"sync_state\"." - } - }, - "tunnel": { - "data": { - "host": "V\u00e4rd", - "port": "Port", - "tunneling_type": "KNX tunneltyp" - }, - "data_description": { - "host": "IP adress till KNX/IP tunnelingsenhet.", - "port": "Port p\u00e5 KNX/IP tunnelingsenhet." - } } } } diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 6ed12e70aee..e1ddb01e317 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -50,24 +50,10 @@ }, "description": "L\u00fctfen `.knxkeys` dosyan\u0131z i\u00e7in bilgileri girin." }, - "secure_manual": { - "data": { - "device_authentication": "Cihaz do\u011frulama \u015fifresi", - "user_id": "Kullan\u0131c\u0131 Kimli\u011fi", - "user_password": "Kullan\u0131c\u0131 \u015fifresi" - }, - "data_description": { - "device_authentication": "Bu, ETS'deki aray\u00fcz\u00fcn 'IP' panelinde ayarlan\u0131r.", - "user_id": "Bu genellikle t\u00fcnel numaras\u0131 +1'dir. Yani 'T\u00fcnel 2' Kullan\u0131c\u0131 Kimli\u011fi '3' olacakt\u0131r.", - "user_password": "ETS'de t\u00fcnelin '\u00d6zellikler' panelinde ayarlanan belirli t\u00fcnel ba\u011flant\u0131s\u0131 i\u00e7in \u015fifre." - }, - "description": "L\u00fctfen IP g\u00fcvenli bilgilerinizi giriniz." - }, "secure_tunneling": { "description": "KNX/IP Secure'u nas\u0131l yap\u0131land\u0131rmak istedi\u011finizi se\u00e7in.", "menu_options": { - "secure_knxkeys": "IP g\u00fcvenli anahtarlar\u0131 i\u00e7eren bir \".knxkeys\" dosyas\u0131 kullan\u0131n", - "secure_manual": "IP g\u00fcvenli anahtarlar\u0131n\u0131 manuel olarak yap\u0131land\u0131r\u0131n" + "secure_knxkeys": "IP g\u00fcvenli anahtarlar\u0131 i\u00e7eren bir \".knxkeys\" dosyas\u0131 kullan\u0131n" } }, "tunnel": { @@ -75,46 +61,6 @@ "gateway": "KNX T\u00fcnel Ba\u011flant\u0131s\u0131" }, "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in." - }, - "type": { - "data": { - "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc" - }, - "description": "L\u00fctfen KNX ba\u011flant\u0131n\u0131z i\u00e7in kullanmam\u0131z gereken ba\u011flant\u0131 tipini giriniz.\n OTOMAT\u0130K - Entegrasyon, bir a\u011f ge\u00e7idi taramas\u0131 ger\u00e7ekle\u015ftirerek KNX Bus'\u0131n\u0131za olan ba\u011flant\u0131y\u0131 halleder.\n T\u00dcNELLEME - Entegrasyon, t\u00fcnelleme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.\n Y\u00d6NLEND\u0130RME - Entegrasyon, y\u00f6nlendirme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r." - } - } - }, - "options": { - "step": { - "init": { - "data": { - "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc", - "individual_address": "Varsay\u0131lan bireysel adres", - "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", - "multicast_group": "\u00c7ok noktaya yay\u0131n grubu", - "multicast_port": "\u00c7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", - "rate_limit": "H\u0131z s\u0131n\u0131r\u0131", - "state_updater": "Durum g\u00fcncelleyici" - }, - "data_description": { - "individual_address": "Home Assistant taraf\u0131ndan kullan\u0131lacak KNX adresi, \u00f6r. \"0.0.4\"", - "local_ip": "Otomatik ke\u015fif i\u00e7in \"0.0.0.0\"\u0131 kullan\u0131n.", - "multicast_group": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131l\u0131r. Varsay\u0131lan: \"224.0.23.12\"", - "multicast_port": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131l\u0131r. Varsay\u0131lan: \"3671\"", - "rate_limit": "Saniyede maksimum giden telegram say\u0131s\u0131.\n \u00d6nerilen: 20 ila 40", - "state_updater": "KNX Bus'tan okuma durumlar\u0131 i\u00e7in varsay\u0131lan\u0131 ayarlay\u0131n. Devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131nda, Home Assistant varl\u0131k durumlar\u0131n\u0131 KNX Bus'tan aktif olarak almaz. 'sync_state' varl\u0131k se\u00e7enekleri taraf\u0131ndan ge\u00e7ersiz k\u0131l\u0131nabilir." - } - }, - "tunnel": { - "data": { - "host": "Sunucu", - "port": "Port", - "tunneling_type": "KNX T\u00fcnel Tipi" - }, - "data_description": { - "host": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n IP adresi.", - "port": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n ba\u011flant\u0131 noktas\u0131." - } } } } diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index b348f38701d..bd11d7f78a0 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -7,22 +7,33 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", + "invalid_backbone_key": "Backbone \u91d1\u9470\u7121\u6548\u3002\u61c9\u70ba 32 \u500b\u5341\u516d\u9032\u4f4d\u6578\u5b57\u3002", "invalid_individual_address": "\u6578\u503c\u8207 KNX \u500b\u5225\u4f4d\u5740\u4e0d\u76f8\u7b26\u3002\n'area.line.device'", "invalid_ip_address": "IPv4 \u4f4d\u5740\u7121\u6548\u3002", - "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002" + "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", + "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002" }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX \u9023\u7dda\u985e\u578b" + }, + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + }, "manual_tunnel": { "data": { "host": "\u4e3b\u6a5f\u7aef", "local_ip": "Home Assistant \u672c\u5730\u7aef IP", "port": "\u901a\u8a0a\u57e0", + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, "data_description": { "host": "KNX/IP \u901a\u9053\u88dd\u7f6e IP \u4f4d\u5740\u3002", "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002", - "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002" + "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", + "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" }, "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" }, @@ -31,7 +42,8 @@ "individual_address": "\u500b\u5225\u4f4d\u5740", "local_ip": "Home Assistant \u672c\u5730\u7aef IP", "multicast_group": "Multicast \u7fa4\u7d44", - "multicast_port": "Multicast \u901a\u8a0a\u57e0" + "multicast_port": "Multicast \u901a\u8a0a\u57e0", + "routing_secure": "\u4f7f\u7528 KNX IP \u52a0\u5bc6" }, "data_description": { "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", @@ -39,6 +51,14 @@ }, "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002" }, + "secure_key_source": { + "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", + "menu_options": { + "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", + "secure_routing_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6 backbone \u91d1\u9470", + "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u6191\u8b49" + } + }, "secure_knxkeys": { "data": { "knxkeys_filename": "`.knxkeys` \u6a94\u6848\u5168\u540d\uff08\u5305\u542b\u526f\u6a94\u540d\uff09", @@ -50,7 +70,18 @@ }, "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002" }, - "secure_manual": { + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone \u91d1\u9470", + "sync_latency_tolerance": "\u7db2\u8def\u5ef6\u9072\u5bb9\u5fcd\u5ea6" + }, + "data_description": { + "backbone_key": "\u65bc ETS \u9805\u76ee\u7684 'Security' \u56de\u5831\u4e2d\u767c\u73fe\u3002\u4f8b\u5982\uff1a'00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "\u9810\u8a2d\u503c\u70ba 1000\u3002" + }, + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + }, + "secure_tunnel_manual": { "data": { "device_authentication": "\u88dd\u7f6e\u8a8d\u8b49\u5bc6\u78bc", "user_id": "\u4f7f\u7528\u8005 ID", @@ -67,7 +98,7 @@ "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", "menu_options": { "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", - "secure_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000" + "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000" } }, "tunnel": { @@ -75,46 +106,128 @@ "gateway": "KNX \u901a\u9053\u9023\u7dda" }, "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" - }, - "type": { - "data": { - "connection_type": "KNX \u9023\u7dda\u985e\u5225" - }, - "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" } } }, "options": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", + "invalid_backbone_key": "Backbone \u91d1\u9470\u7121\u6548\u3002\u61c9\u70ba 32 \u500b\u5341\u516d\u9032\u4f4d\u6578\u5b57\u3002", + "invalid_individual_address": "\u6578\u503c\u8207 KNX \u500b\u5225\u4f4d\u5740\u4e0d\u76f8\u7b26\u3002\n'area.line.device'", + "invalid_ip_address": "IPv4 \u4f4d\u5740\u7121\u6548\u3002", + "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", + "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002" + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "KNX \u9023\u7dda\u985e\u5225", - "individual_address": "\u9810\u8a2d\u500b\u5225\u4f4d\u5740", - "local_ip": "Home Assistant \u672c\u5730\u7aef IP", - "multicast_group": "Multicast \u7fa4\u7d44", - "multicast_port": "Multicast \u901a\u8a0a\u57e0", "rate_limit": "\u983b\u7387\u9650\u5236", - "state_updater": "\u88dd\u614b\u66f4\u65b0\u5668" + "state_updater": "\u72c0\u614b\u66f4\u65b0\u5668" }, "data_description": { - "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", - "local_ip": "\u4f7f\u7528 `0.0.0.0` \u9032\u884c\u81ea\u52d5\u641c\u7d22\u3002", - "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u81ea\u52d5\u641c\u7d22\u3002\u9810\u8a2d\u503c\uff1a`224.0.23.12`", - "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u81ea\u52d5\u641c\u7d22\u3002\u9810\u8a2d\u503c\uff1a`3671`", - "rate_limit": "\u6bcf\u79d2\u6700\u5927 Telegram \u767c\u9001\u91cf\u3002\u5efa\u8b70\uff1a20 - 40", + "rate_limit": "\u6bcf\u79d2\u6700\u5927 Telegram \u767c\u9001\u91cf\u3002\n`0` \u70ba\u95dc\u9589\u9650\u5236\u3001\u5efa\u8b70\uff1a0 \u6216 20 \u81f3 40", "state_updater": "\u8a2d\u5b9a\u9810\u8a2d KNX Bus \u8b80\u53d6\u72c0\u614b\u3002\u7576\u95dc\u9589\u6642\u3001Home Assistant \u5c07\u4e0d\u6703\u4e3b\u52d5\u5f9e KNX Bus \u7372\u53d6\u5be6\u9ad4\u72c0\u614b\uff0c\u53ef\u88ab`sync_state` \u5be6\u9ad4\u9078\u9805\u8986\u84cb\u3002" } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "KNX \u9023\u7dda\u985e\u578b" + }, + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + }, + "manual_tunnel": { "data": { "host": "\u4e3b\u6a5f\u7aef", + "local_ip": "Home Assistant \u672c\u5730\u7aef IP", "port": "\u901a\u8a0a\u57e0", + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, "data_description": { "host": "KNX/IP \u901a\u9053\u88dd\u7f6e IP \u4f4d\u5740\u3002", - "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002" + "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002", + "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", + "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" + }, + "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" + }, + "options_init": { + "menu_options": { + "communication_settings": "\u901a\u8a0a\u8a2d\u5b9a", + "connection_type": "\u8a2d\u5b9a KNX \u4ecb\u9762" } + }, + "routing": { + "data": { + "individual_address": "\u500b\u5225\u4f4d\u5740", + "local_ip": "Home Assistant \u672c\u5730\u7aef IP", + "multicast_group": "Multicast \u7fa4\u7d44", + "multicast_port": "Multicast \u901a\u8a0a\u57e0", + "routing_secure": "\u4f7f\u7528 KNX IP \u52a0\u5bc6" + }, + "data_description": { + "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", + "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002" + }, + "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002" + }, + "secure_key_source": { + "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", + "menu_options": { + "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", + "secure_routing_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6 backbone \u91d1\u9470", + "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u6191\u8b49" + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "`.knxkeys` \u6a94\u6848\u5168\u540d\uff08\u5305\u542b\u526f\u6a94\u540d\uff09", + "knxkeys_password": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc" + }, + "data_description": { + "knxkeys_filename": "\u6a94\u6848\u61c9\u8a72\u4f4d\u65bc\u8a2d\u5b9a\u8cc7\u6599\u593e `.storage/knx/` \u5167\u3002\n\u82e5\u70ba Home Assistant OS\u3001\u5247\u61c9\u8a72\u70ba `/config/.storage/knx/`\n\u4f8b\u5982\uff1a`my_project.knxkeys`", + "knxkeys_password": "\u81ea ETS \u532f\u51fa\u6a94\u6848\u4e2d\u9032\u884c\u8a2d\u5b9a\u3002" + }, + "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002" + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Backbone \u91d1\u9470", + "sync_latency_tolerance": "\u7db2\u8def\u5ef6\u9072\u5bb9\u5fcd\u5ea6" + }, + "data_description": { + "backbone_key": "\u65bc ETS \u9805\u76ee\u7684 'Security' \u56de\u5831\u4e2d\u767c\u73fe\u3002\u4f8b\u5982\uff1a'00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "\u9810\u8a2d\u503c\u70ba 1000\u3002" + }, + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "\u88dd\u7f6e\u8a8d\u8b49\u5bc6\u78bc", + "user_id": "\u4f7f\u7528\u8005 ID", + "user_password": "\u4f7f\u7528\u8005\u5bc6\u78bc" + }, + "data_description": { + "device_authentication": "\u65bc EST \u4ecb\u9762\u4e2d 'IP' \u9762\u677f\u9032\u884c\u8a2d\u5b9a\u3002", + "user_id": "\u901a\u5e38\u70ba\u901a\u9053\u6578 +1\u3002\u56e0\u6b64 'Tunnel 2' \u5c07\u5177\u6709\u4f7f\u7528\u8005 ID '3'\u3002", + "user_password": "\u65bc ETS \u901a\u9053 'Properties' \u9762\u677f\u53ef\u8a2d\u5b9a\u6307\u5b9a\u901a\u9053\u9023\u7dda\u5bc6\u78bc\u3002" + }, + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + }, + "secure_tunneling": { + "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", + "menu_options": { + "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", + "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000" + } + }, + "tunnel": { + "data": { + "gateway": "KNX \u901a\u9053\u9023\u7dda" + }, + "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" } } } diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 32f37ad2ac2..92034be95ff 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -9,10 +9,10 @@ from homeassistant.components.weather import WeatherEntity from homeassistant.const import ( CONF_ENTITY_CATEGORY, CONF_NAME, - PRESSURE_PA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, Platform, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -75,9 +75,9 @@ class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather - _attr_native_pressure_unit = PRESSURE_PA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_pressure_unit = UnitOfPressure.PA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" diff --git a/homeassistant/components/kodi/translations/sk.json b/homeassistant/components/kodi/translations/sk.json index ab39cbe9c5e..be6f0128d39 100644 --- a/homeassistant/components/kodi/translations/sk.json +++ b/homeassistant/components/kodi/translations/sk.json @@ -2,20 +2,31 @@ "config": { "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "flow_title": "{name}", "step": { "credentials": { "data": { + "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } }, + "discovery_confirm": { + "title": "Objaven\u00e9 Kodi" + }, "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t" } }, "ws_port": { @@ -24,5 +35,11 @@ } } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} bola po\u017eiadan\u00e1 o vypnutie", + "turn_on": "{entity_name} bola po\u017eiadan\u00e1 o zapnutie" + } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/sk.json b/homeassistant/components/konnected/translations/sk.json index 51eaba460d8..d84d42532f9 100644 --- a/homeassistant/components/konnected/translations/sk.json +++ b/homeassistant/components/konnected/translations/sk.json @@ -1,31 +1,73 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "not_konn_panel": "Nie je rozpoznan\u00e9 zariadenie Konnected.io", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" }, "step": { + "confirm": { + "description": "Model: {model}\n ID: {id}\n Hostite\u013e: {host}\n Port: {port} \n\n Spr\u00e1vanie IO a panela m\u00f4\u017eete nakonfigurova\u0165 v nastaveniach panela Pripojen\u00fd alarm." + }, "user": { "data": { + "host": "IP adresa", "port": "Port" } } } }, "options": { + "abort": { + "not_konn_panel": "Nie je rozpoznan\u00e9 zariadenie Konnected.io" + }, + "error": { + "bad_host": "Neplatn\u00e1 adresa URL hostite\u013ea API Override" + }, "step": { "options_binary": { "data": { - "name": "N\u00e1zov (volite\u013en\u00fd)" - } + "inverse": "Invertujte stav otvoren\u00e9/zatvoren\u00e9", + "name": "N\u00e1zov (volite\u013en\u00fd)", + "type": "Typ sn\u00edma\u010da" + }, + "description": "mo\u017enosti {zone}", + "title": "Konfigur\u00e1cia sn\u00edma\u010da" }, "options_digital": { "data": { - "name": "N\u00e1zov (volite\u013en\u00fd)" + "name": "N\u00e1zov (volite\u013en\u00fd)", + "type": "Typ sn\u00edma\u010da" + }, + "description": "mo\u017enosti {zone}" + }, + "options_io": { + "data": { + "1": "Z\u00f3na 1", + "2": "Z\u00f3na 2", + "3": "Z\u00f3na 3", + "4": "Z\u00f3na 4", + "5": "Z\u00f3na 5", + "6": "Z\u00f3na 6", + "7": "Z\u00f3na 7" + }, + "title": "Konfigur\u00e1cia I/O" + }, + "options_misc": { + "data": { + "api_host": "Prep\u00edsanie adresy URL hostite\u013ea API" } }, "options_switch": { "data": { - "name": "N\u00e1zov (volite\u013en\u00fd)" + "momentary": "Trvanie impulzu (ms)", + "name": "N\u00e1zov (volite\u013en\u00fd)", + "pause": "Pauza medzi impulzmi (ms)" } } } diff --git a/homeassistant/components/kostal_plenticore/translations/sk.json b/homeassistant/components/kostal_plenticore/translations/sk.json index 5ada995aa6e..2ee96a72a30 100644 --- a/homeassistant/components/kostal_plenticore/translations/sk.json +++ b/homeassistant/components/kostal_plenticore/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/cs.json b/homeassistant/components/kraken/translations/cs.json new file mode 100644 index 00000000000..7f5cce1ca73 --- /dev/null +++ b/homeassistant/components/kraken/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/sk.json b/homeassistant/components/kraken/translations/sk.json index ba5e13223a7..715c2e1496d 100644 --- a/homeassistant/components/kraken/translations/sk.json +++ b/homeassistant/components/kraken/translations/sk.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "already_configured": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/kulersky/translations/he.json b/homeassistant/components/kulersky/translations/he.json index d3d68dccc93..4eafc6dc29b 100644 --- a/homeassistant/components/kulersky/translations/he.json +++ b/homeassistant/components/kulersky/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/kulersky/translations/sk.json b/homeassistant/components/kulersky/translations/sk.json new file mode 100644 index 00000000000..d4bb209c34c --- /dev/null +++ b/homeassistant/components/kulersky/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/bg.json b/homeassistant/components/lacrosse_view/translations/bg.json index 652dea38fcc..057f117c82a 100644 --- a/homeassistant/components/lacrosse_view/translations/bg.json +++ b/homeassistant/components/lacrosse_view/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/lacrosse_view/translations/he.json b/homeassistant/components/lacrosse_view/translations/he.json index fe6357d0150..081dd5a3725 100644 --- a/homeassistant/components/lacrosse_view/translations/he.json +++ b/homeassistant/components/lacrosse_view/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", diff --git a/homeassistant/components/lacrosse_view/translations/sk.json b/homeassistant/components/lacrosse_view/translations/sk.json new file mode 100644 index 00000000000..af696ee1cfe --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie", + "no_locations": "Nena\u0161li sa \u017eiadne lokality", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/bg.json b/homeassistant/components/lametric/translations/bg.json index 6f85c1ddaf5..a886ec4fe9a 100644 --- a/homeassistant/components/lametric/translations/bg.json +++ b/homeassistant/components/lametric/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/lametric/translations/cs.json b/homeassistant/components/lametric/translations/cs.json index 59280c6c2bc..3b3f849ac95 100644 --- a/homeassistant/components/lametric/translations/cs.json +++ b/homeassistant/components/lametric/translations/cs.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", - "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})" + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json index 15d98a45dbb..5b7c2534c5d 100644 --- a/homeassistant/components/lametric/translations/de.json +++ b/homeassistant/components/lametric/translations/de.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "invalid_discovery_info": "Ung\u00fcltige Suchinformationen erhalten", "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", - "missing_configuration": "Die LaMetric-Integration ist nicht konfiguriert. Bitte folge der Dokumentation.", + "missing_configuration": "Die LaMetric Integration ist nicht konfiguriert. Bitte folge der Dokumentation.", "no_devices": "Der autorisierte Benutzer hat keine LaMetric Ger\u00e4te", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "reauth_device_not_found": "Das Ger\u00e4t, das du erneut authentifizieren m\u00f6chtest, wird in diesem LaMetric-Konto nicht gefunden", @@ -18,7 +18,7 @@ }, "step": { "choice_enter_manual_or_fetch_cloud": { - "description": "Ein LaMetric-Ger\u00e4t kann im Home Assistant auf zwei verschiedene Arten eingerichtet werden.\n\nDu kannst alle Ger\u00e4teinformationen und API-Tokens selbst eingeben, oder Home Assistant kann sie von deinem LaMetric.com-Konto importieren.", + "description": "Ein LaMetric Ger\u00e4t kann im Home Assistant auf zwei verschiedene Arten eingerichtet werden.\n\nDu kannst alle Ger\u00e4teinformationen und API-Tokens selbst eingeben oder Home Assistant kann sie von deinem LaMetric.com Konto importieren.", "menu_options": { "manual_entry": "Manuell eintragen", "pick_implementation": "Import von LaMetric.com (empfohlen)" @@ -30,7 +30,7 @@ "host": "Host" }, "data_description": { - "api_key": "Du findest diesen API-Schl\u00fcssel auf der [Ger\u00e4teseite in deinem LaMetric-Entwicklerkonto](https://developer.lametric.com/user/devices).", + "api_key": "Du findest diesen API-Schl\u00fcssel auf der [Ger\u00e4teseite in deinem LaMetric Entwicklerkonto](https://developer.lametric.com/user/devices).", "host": "Die IP-Adresse oder der Hostname deines LaMetric TIME in deinem Netzwerk." } }, @@ -39,7 +39,7 @@ }, "user_cloud_select_device": { "data": { - "device": "W\u00e4hle das hinzuzuf\u00fcgende LaMetric-Ger\u00e4t aus" + "device": "W\u00e4hle das hinzuzuf\u00fcgende LaMetric Ger\u00e4t aus" } } } diff --git a/homeassistant/components/lametric/translations/el.json b/homeassistant/components/lametric/translations/el.json index 8733a19e690..dfd0423ce15 100644 --- a/homeassistant/components/lametric/translations/el.json +++ b/homeassistant/components/lametric/translations/el.json @@ -8,6 +8,8 @@ "missing_configuration": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "no_devices": "\u039f \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 LaMetric", "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", + "reauth_device_not_found": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03c4\u03b5 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bd \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc LaMetric", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { diff --git a/homeassistant/components/lametric/translations/he.json b/homeassistant/components/lametric/translations/he.json index 97c060f6062..0912073e131 100644 --- a/homeassistant/components/lametric/translations/he.json +++ b/homeassistant/components/lametric/translations/he.json @@ -10,6 +10,17 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "manual_entry": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "host": "\u05de\u05d0\u05e8\u05d7" + } + }, + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/hr.json b/homeassistant/components/lametric/translations/hr.json new file mode 100644 index 00000000000..27a5722e0b4 --- /dev/null +++ b/homeassistant/components/lametric/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponovna provjera autenti\u010dnosti je uspje\u0161na" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.el.json b/homeassistant/components/lametric/translations/select.el.json new file mode 100644 index 00000000000..f51845f4c45 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.el.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "manual": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.hr.json b/homeassistant/components/lametric/translations/select.hr.json new file mode 100644 index 00000000000..b4957eae992 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.hr.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatski", + "manual": "Ru\u010dno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/sk.json b/homeassistant/components/lametric/translations/sk.json new file mode 100644 index 00000000000..e2a8b9c5051 --- /dev/null +++ b/homeassistant/components/lametric/translations/sk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "invalid_discovery_info": "Prijat\u00e9 neplatn\u00e9 inform\u00e1cie o zis\u0165ovan\u00ed", + "link_local_address": "Lok\u00e1lne adresy odkazov nie s\u00fa podporovan\u00e9", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "Zadajte manu\u00e1lne" + } + }, + "manual_entry": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "host": "Hostite\u013e" + } + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 3ef235ff8af..34724c07ca9 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -4,11 +4,12 @@ from __future__ import annotations from datetime import timedelta import logging -from ultraheat_api import HeatMeterService, UltraheatReader +import ultraheat_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_registry import async_migrate_entries from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -22,13 +23,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up heat meter from a config entry.""" _LOGGER.debug("Initializing %s integration on %s", DOMAIN, entry.data[CONF_DEVICE]) - reader = UltraheatReader(entry.data[CONF_DEVICE]) - - api = HeatMeterService(reader) + reader = ultraheat_api.UltraheatReader(entry.data[CONF_DEVICE]) + api = ultraheat_api.HeatMeterService(reader) async def async_update_data(): """Fetch data from the API.""" - _LOGGER.info("Polling on %s", entry.data[CONF_DEVICE]) + _LOGGER.debug("Polling on %s", entry.data[CONF_DEVICE]) return await hass.async_add_executor_job(api.read) # Polling is only daily to prevent battery drain. @@ -53,3 +53,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + # Removing domain name and config entry id from entity unique id's, replacing it with device number + if config_entry.version == 1: + + config_entry.version = 2 + + device_number = config_entry.data["device_number"] + + @callback + def update_entity_unique_id(entity_entry): + """Update unique ID of entity entry.""" + if entity_entry.platform in entity_entry.unique_id: + return { + "new_unique_id": entity_entry.unique_id.replace( + f"{entity_entry.platform}_{entity_entry.config_entry_id}", + f"{device_number}", + ) + } + + await async_migrate_entries( + hass, config_entry.entry_id, update_entity_unique_id + ) + hass.config_entries.async_update_entry(config_entry) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index 2e244a9a65f..f12992166fb 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -1,17 +1,21 @@ """Config flow for Landis+Gyr Heat Meter integration.""" from __future__ import annotations +import asyncio import logging -import os +from typing import Any import async_timeout import serial -import serial.tools.list_ports -from ultraheat_api import HeatMeterService, UltraheatReader +from serial.tools import list_ports +import ultraheat_api import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import usb from homeassistant.const import CONF_DEVICE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN, ULTRAHEAT_TIMEOUT @@ -30,9 +34,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema( class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Ultraheat Heat Meter.""" - VERSION = 1 + VERSION = 2 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Step when setting up serial configuration.""" errors = {} @@ -41,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_setup_serial_manual_path() dev_path = await self.hass.async_add_executor_job( - get_serial_by_id, user_input[CONF_DEVICE] + usb.get_serial_by_id, user_input[CONF_DEVICE] ) _LOGGER.debug("Using this path : %s", dev_path) @@ -50,12 +56,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except CannotConnect: errors["base"] = "cannot_connect" - ports = await self.get_ports() + ports = await get_usb_ports(self.hass) + ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(ports)}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_setup_serial_manual_path(self, user_input=None): + async def async_step_setup_serial_manual_path( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Set path manually.""" errors = {} @@ -78,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): model, device_number = await self.validate_ultraheat(dev_path) _LOGGER.debug("Got model %s and device_number %s", model, device_number) - await self.async_set_unique_id(device_number) + await self.async_set_unique_id(f"{device_number}") self._abort_if_unique_id_configured() data = { CONF_DEVICE: dev_path, @@ -90,48 +99,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data=data, ) - async def validate_ultraheat(self, port: str): + async def validate_ultraheat(self, port: str) -> tuple[str, str]: """Validate the user input allows us to connect.""" - reader = UltraheatReader(port) - heat_meter = HeatMeterService(reader) + reader = ultraheat_api.UltraheatReader(port) + heat_meter = ultraheat_api.HeatMeterService(reader) try: async with async_timeout.timeout(ULTRAHEAT_TIMEOUT): # validate and retrieve the model and device number for a unique id data = await self.hass.async_add_executor_job(heat_meter.read) - _LOGGER.debug("Got data from Ultraheat API: %s", data) - except Exception as err: + except (asyncio.TimeoutError, serial.serialutil.SerialException) as err: _LOGGER.warning("Failed read data from: %s. %s", port, err) raise CannotConnect(f"Error communicating with device: {err}") from err - _LOGGER.debug("Successfully connected to %s", port) + _LOGGER.debug("Successfully connected to %s. Got data: %s", port, data) return data.model, data.device_number - async def get_ports(self) -> dict: - """Get the available ports.""" - ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) - formatted_ports = {} - for port in ports: - formatted_ports[ - port.device - ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( - f" - {port.manufacturer}" if port.manufacturer else "" + +async def get_usb_ports(hass: HomeAssistant) -> dict[str, str]: + """Return a dict of USB ports and their friendly names.""" + ports = await hass.async_add_executor_job(list_ports.comports) + port_descriptions = {} + for port in ports: + # this prevents an issue with usb_device_from_port not working for ports without vid on RPi + if port.vid: + usb_device = usb.usb_device_from_port(port) + dev_path = usb.get_serial_by_id(usb_device.device) + human_name = usb.human_readable_device_name( + dev_path, + usb_device.serial_number, + usb_device.manufacturer, + usb_device.description, + usb_device.vid, + usb_device.pid, ) - formatted_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH - return formatted_ports + port_descriptions[dev_path] = human_name - -def get_serial_by_id(dev_path: str) -> str: - """Return a /dev/serial/by-id match for given device if available.""" - by_id = "/dev/serial/by-id" - if not os.path.isdir(by_id): - return dev_path - - for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): - if os.path.realpath(path) == dev_path: - return path - return dev_path + return port_descriptions class CannotConnect(HomeAssistantError): diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 57a8f9d9be4..7767a491f3b 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -5,7 +5,15 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import ENERGY_MEGA_WATT_HOUR, TEMP_CELSIUS, VOLUME_CUBIC_METERS +from homeassistant.const import ( + ENERGY_MEGA_WATT_HOUR, + POWER_KILO_WATT, + TEMP_CELSIUS, + TIME_HOURS, + TIME_MINUTES, + VOLUME_CUBIC_METERS, + VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, +) from homeassistant.helpers.entity import EntityCategory DOMAIN = "landisgyr_heat_meter" @@ -26,6 +34,7 @@ HEAT_METER_SENSOR_TYPES = ( key="volume_usage_m3", icon="mdi:fire", name="Volume usage", + device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=SensorStateClass.TOTAL, ), @@ -56,12 +65,14 @@ HEAT_METER_SENSOR_TYPES = ( key="volume_previous_year_m3", icon="mdi:fire", name="Volume usage previous year", + device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=VOLUME_CUBIC_METERS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="ownership_number", name="Ownership number", + icon="mdi:identifier", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -73,41 +84,41 @@ HEAT_METER_SENSOR_TYPES = ( SensorEntityDescription( key="device_number", name="Device number", + icon="mdi:identifier", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="measurement_period_minutes", name="Measurement period minutes", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_MINUTES, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="power_max_kw", name="Power max", - native_unit_of_measurement="kW", - icon="mdi:power-plug-outline", + native_unit_of_measurement=POWER_KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="power_max_previous_year_kw", name="Power max previous year", - native_unit_of_measurement="kW", - icon="mdi:power-plug-outline", + native_unit_of_measurement=POWER_KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="flowrate_max_m3ph", name="Flowrate max", - native_unit_of_measurement="m3ph", + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="flowrate_max_previous_year_m3ph", name="Flowrate max previous year", - native_unit_of_measurement="m3ph", + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -115,7 +126,6 @@ HEAT_METER_SENSOR_TYPES = ( key="return_temperature_max_c", name="Return temperature max", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -123,7 +133,6 @@ HEAT_METER_SENSOR_TYPES = ( key="return_temperature_max_previous_year_c", name="Return temperature max previous year", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -131,7 +140,6 @@ HEAT_METER_SENSOR_TYPES = ( key="flow_temperature_max_c", name="Flow temperature max", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -139,32 +147,35 @@ HEAT_METER_SENSOR_TYPES = ( key="flow_temperature_max_previous_year_c", name="Flow temperature max previous year", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="operating_hours", name="Operating hours", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="flow_hours", name="Flow hours", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="fault_hours", name="Fault hours", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="fault_hours_previous_year", name="Fault hours previous year", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -189,7 +200,7 @@ HEAT_METER_SENSOR_TYPES = ( SensorEntityDescription( key="measuring_range_m3ph", name="Measuring range", - native_unit_of_measurement="m3ph", + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index dc6444b478d..a20225c88b0 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -3,11 +3,12 @@ "name": "Landis+Gyr Heat Meter", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter", - "requirements": ["ultraheat-api==0.5.0"], + "requirements": ["ultraheat-api==0.5.1"], "ssdp": [], "zeroconf": [], "homekit": {}, "dependencies": [], "codeowners": ["@vpathuis"], + "dependencies": ["usb"], "iot_class": "local_polling" } diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 23a6e217458..2b4fc6edea8 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -4,13 +4,8 @@ from __future__ import annotations from dataclasses import asdict import logging -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - RestoreSensor, - SensorDeviceClass, -) +from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,8 +22,6 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensor platform.""" - _LOGGER.info("The Landis+Gyr Heat Meter sensor platform is being set up!") - unique_id = entry.entry_id coordinator = hass.data[DOMAIN][entry.entry_id] @@ -44,7 +37,7 @@ async def async_setup_entry( sensors = [] for description in HEAT_METER_SENSOR_TYPES: - sensors.append(HeatMeterSensor(coordinator, unique_id, description, device)) + sensors.append(HeatMeterSensor(coordinator, description, device)) async_add_entities(sensors) @@ -52,24 +45,16 @@ async def async_setup_entry( class HeatMeterSensor(CoordinatorEntity, RestoreSensor): """Representation of a Sensor.""" - def __init__(self, coordinator, unique_id, description, device): + def __init__(self, coordinator, description, device): """Set up the sensor with the initial values.""" super().__init__(coordinator) self.key = description.key - self._attr_unique_id = f"{DOMAIN}_{unique_id}_{description.key}" - self._attr_name = "Heat Meter " + description.name - if hasattr(description, "icon"): - self._attr_icon = description.icon - if hasattr(description, "entity_category"): - self._attr_entity_category = description.entity_category - if hasattr(description, ATTR_STATE_CLASS): - self._attr_state_class = description.state_class - if hasattr(description, ATTR_DEVICE_CLASS): - self._attr_device_class = description.device_class - if hasattr(description, ATTR_UNIT_OF_MEASUREMENT): - self._attr_native_unit_of_measurement = ( - description.native_unit_of_measurement - ) + self._attr_unique_id = ( + f"{coordinator.config_entry.data['device_number']}_{description.key}" + ) + self._attr_name = f"Heat Meter {description.name}" + self.entity_description = description + self._attr_device_info = device self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year")) diff --git a/homeassistant/components/landisgyr_heat_meter/strings.json b/homeassistant/components/landisgyr_heat_meter/strings.json index 61e170af2b3..4bae2490006 100644 --- a/homeassistant/components/landisgyr_heat_meter/strings.json +++ b/homeassistant/components/landisgyr_heat_meter/strings.json @@ -12,10 +12,6 @@ } } }, - "error": { - "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%]" } diff --git a/homeassistant/components/landisgyr_heat_meter/translations/bg.json b/homeassistant/components/landisgyr_heat_meter/translations/bg.json index 6120c9152d7..8ae6c2c37ca 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/bg.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/bg.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, - "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ca.json b/homeassistant/components/landisgyr_heat_meter/translations/ca.json index 1ad4d3b9362..aaa12ad2b85 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/ca.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/ca.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat" }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "unknown": "Error inesperat" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/cs.json b/homeassistant/components/landisgyr_heat_meter/translations/cs.json index 500211d103c..ec3ffb78c6f 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/cs.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/cs.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, - "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/de.json b/homeassistant/components/landisgyr_heat_meter/translations/de.json index e8a48a02c51..3c162b69e3a 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/de.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/de.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "unknown": "Unerwarteter Fehler" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/el.json b/homeassistant/components/landisgyr_heat_meter/translations/el.json index 1f7d7b27df9..081888c7a5d 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/el.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/el.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/en.json b/homeassistant/components/landisgyr_heat_meter/translations/en.json index 84caa2819a4..d2e4bff5ae2 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/en.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/en.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Device is already configured" }, - "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/es.json b/homeassistant/components/landisgyr_heat_meter/translations/es.json index 956cebf852d..dc9fd6c9e1d 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/es.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/es.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado" }, - "error": { - "cannot_connect": "No se pudo conectar", - "unknown": "Error inesperado" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/et.json b/homeassistant/components/landisgyr_heat_meter/translations/et.json index 9553db6f7ca..ce0e6b5d759 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/et.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/et.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "unknown": "Ootamatu t\u00f5rge" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/fr.json b/homeassistant/components/landisgyr_heat_meter/translations/fr.json index 42d2fe61555..02c3ec487ad 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/fr.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/fr.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, - "error": { - "cannot_connect": "\u00c9chec de connexion", - "unknown": "Erreur inattendue" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/he.json b/homeassistant/components/landisgyr_heat_meter/translations/he.json new file mode 100644 index 00000000000..1c5713312c7 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "user": { + "data": { + "device": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/hu.json b/homeassistant/components/landisgyr_heat_meter/translations/hu.json index 030fe6853f2..5e04076bf2a 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/hu.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/hu.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/id.json b/homeassistant/components/landisgyr_heat_meter/translations/id.json index 97bb43eb4ba..47102d37e1c 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/id.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/id.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi" }, - "error": { - "cannot_connect": "Gagal terhubung", - "unknown": "Kesalahan yang tidak diharapkan" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/it.json b/homeassistant/components/landisgyr_heat_meter/translations/it.json index 1c320671a40..6b1cd3f8f5c 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/it.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/it.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, - "error": { - "cannot_connect": "Impossibile connettersi", - "unknown": "Errore imprevisto" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ja.json b/homeassistant/components/landisgyr_heat_meter/translations/ja.json index b9ac41b8244..0fd5a1c5752 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/ja.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/ja.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/nb.json b/homeassistant/components/landisgyr_heat_meter/translations/nb.json deleted file mode 100644 index a22f7eef3d6..00000000000 --- a/homeassistant/components/landisgyr_heat_meter/translations/nb.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "unknown": "Uventet feil" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/nl.json b/homeassistant/components/landisgyr_heat_meter/translations/nl.json index 67eea59125f..436b82d20e1 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/nl.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/nl.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd" }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "unknown": "Onverwachte fout" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/no.json b/homeassistant/components/landisgyr_heat_meter/translations/no.json index 7c3a6e348f3..b583d0b86e0 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/no.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/no.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Enheten er allerede konfigurert" }, - "error": { - "cannot_connect": "Tilkobling mislyktes", - "unknown": "Uventet feil" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pl.json b/homeassistant/components/landisgyr_heat_meter/translations/pl.json index 3478a5c0c2b..f3f5299624d 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/pl.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/pl.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json index 97cced694cf..acb8b394232 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, - "error": { - "cannot_connect": "Falha ao conectar", - "unknown": "Erro inesperado" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ru.json b/homeassistant/components/landisgyr_heat_meter/translations/ru.json index b4977ecdc39..5c358e82b11 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/ru.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/ru.json @@ -3,10 +3,6 @@ "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.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/sk.json b/homeassistant/components/landisgyr_heat_meter/translations/sk.json new file mode 100644 index 00000000000..86de76ef980 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Cesta k zariadeniu USB" + } + }, + "user": { + "data": { + "device": "Vyberte zariadenie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/sv.json b/homeassistant/components/landisgyr_heat_meter/translations/sv.json index 4fde3cf2755..99521c6c4d5 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/sv.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/sv.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad" }, - "error": { - "cannot_connect": "Det gick inte att ansluta.", - "unknown": "Ov\u00e4ntat fel" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/tr.json b/homeassistant/components/landisgyr_heat_meter/translations/tr.json index 1ff9d1c85d0..898ed7c5025 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/tr.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/tr.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json b/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json index 718576d8a26..5a6d25c5a2a 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, "step": { "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/launch_library/translations/sk.json b/homeassistant/components/launch_library/translations/sk.json new file mode 100644 index 00000000000..c294bc45d7c --- /dev/null +++ b/homeassistant/components/launch_library/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/de.json b/homeassistant/components/laundrify/translations/de.json index 016f345e13d..5df595b2dd2 100644 --- a/homeassistant/components/laundrify/translations/de.json +++ b/homeassistant/components/laundrify/translations/de.json @@ -14,10 +14,10 @@ "data": { "code": "Auth-Code (xxx-xxx)" }, - "description": "Bitte gib deinen pers\u00f6nlichen Auth-Code ein, der in der laundrify-App angezeigt wird." + "description": "Bitte gib deinen pers\u00f6nlichen Auth-Code ein, der in der laundrify App angezeigt wird." }, "reauth_confirm": { - "description": "Die laundrify-Integration muss sich neu authentifizieren.", + "description": "Die laundrify Integration muss sich neu authentifizieren.", "title": "Integration erneut authentifizieren" } } diff --git a/homeassistant/components/laundrify/translations/sk.json b/homeassistant/components/laundrify/translations/sk.json new file mode 100644 index 00000000000..f8f52f3f99d --- /dev/null +++ b/homeassistant/components/laundrify/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_format": "Neplatn\u00fd form\u00e1t. Zadajte ako xxx-xxx.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "init": { + "data": { + "code": "Autoriza\u010dn\u00fd k\u00f3d (xxx-xxx)" + } + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/sk.json b/homeassistant/components/lcn/translations/sk.json new file mode 100644 index 00000000000..41b4464c18f --- /dev/null +++ b/homeassistant/components/lcn/translations/sk.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "trigger_type": { + "codelock": "prijat\u00fd k\u00f3d z\u00e1mku", + "fingerprint": "prijat\u00fd k\u00f3d odtla\u010dku prsta", + "send_keys": "odosla\u0165 prijat\u00e9 k\u013e\u00fa\u010de" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/config_flow.py b/homeassistant/components/led_ble/config_flow.py index 19be92f6647..d757b5021af 100644 --- a/homeassistant/components/led_ble/config_flow.py +++ b/homeassistant/components/led_ble/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from bluetooth_data_tools import human_readable_name from led_ble import BLEAK_EXCEPTIONS, LEDBLE import voluptuous as vol @@ -16,7 +17,6 @@ from homeassistant.const import CONF_ADDRESS from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, LOCAL_NAMES, UNSUPPORTED_SUB_MODEL -from .util import human_readable_name _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/led_ble/const.py b/homeassistant/components/led_ble/const.py index ad3ea8f6707..64c28d1ada5 100644 --- a/homeassistant/components/led_ble/const.py +++ b/homeassistant/components/led_ble/const.py @@ -1,5 +1,7 @@ """Constants for the LED BLE integration.""" +from typing import Final + DOMAIN = "led_ble" DEVICE_TIMEOUT = 30 @@ -8,3 +10,5 @@ LOCAL_NAMES = {"LEDnet", "BLE-LED", "LEDBLE", "Triones", "LEDBlue"} UNSUPPORTED_SUB_MODEL = "LEDnetWF" UPDATE_SECONDS = 15 + +DEFAULT_EFFECT_SPEED: Final = 50 diff --git a/homeassistant/components/led_ble/light.py b/homeassistant/components/led_ble/light.py index 4a8ff3f01af..22a52e61b63 100644 --- a/homeassistant/components/led_ble/light.py +++ b/homeassistant/components/led_ble/light.py @@ -7,10 +7,12 @@ from led_ble import LEDBLE from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_WHITE, ColorMode, LightEntity, + LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -22,7 +24,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN +from .const import DEFAULT_EFFECT_SPEED, DOMAIN from .models import LEDBLEData @@ -41,6 +43,7 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity): _attr_supported_color_modes = {ColorMode.RGB, ColorMode.WHITE} _attr_has_entity_name = True + _attr_supported_features = LightEntityFeature.EFFECT def __init__( self, coordinator: DataUpdateCoordinator, device: LEDBLE, name: str @@ -51,7 +54,7 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity): self._attr_unique_id = device.address self._attr_device_info = DeviceInfo( name=name, - model=hex(device.model_num), + model=f"{device.model_data.description} {hex(device.model_num)}", sw_version=hex(device.version_num), connections={(dr.CONNECTION_BLUETOOTH, device.address)}, ) @@ -65,10 +68,23 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity): self._attr_brightness = device.brightness self._attr_rgb_color = device.rgb_unscaled self._attr_is_on = device.on + self._attr_effect = device.effect + self._attr_effect_list = device.effect_list + + async def _async_set_effect(self, effect: str, brightness: int) -> None: + """Set an effect.""" + await self._device.async_set_effect( + effect, + self._device.speed or DEFAULT_EFFECT_SPEED, + round(brightness / 255 * 100), + ) async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) + if effect := kwargs.get(ATTR_EFFECT): + await self._async_set_effect(effect, brightness) + return if ATTR_RGB_COLOR in kwargs: rgb = kwargs[ATTR_RGB_COLOR] await self._device.set_rgb(rgb, brightness) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 6802eea9bc7..ea272dcacc0 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -3,7 +3,7 @@ "name": "LED BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/led_ble/", - "requirements": ["led-ble==1.0.0"], + "requirements": ["bluetooth-data-tools==0.3.0", "led-ble==1.0.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/homeassistant/components/led_ble/translations/cs.json b/homeassistant/components/led_ble/translations/cs.json index 99738ebc78e..ad445a240db 100644 --- a/homeassistant/components/led_ble/translations/cs.json +++ b/homeassistant/components/led_ble/translations/cs.json @@ -8,6 +8,13 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "address": "Bluetooth adresa" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/he.json b/homeassistant/components/led_ble/translations/he.json index 6dc5ae75df8..3f0c3c6d1f2 100644 --- a/homeassistant/components/led_ble/translations/he.json +++ b/homeassistant/components/led_ble/translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/led_ble/translations/sk.json b/homeassistant/components/led_ble/translations/sk.json new file mode 100644 index 00000000000..994e95cb8cd --- /dev/null +++ b/homeassistant/components/led_ble/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "no_unconfigured_devices": "Nena\u0161li sa \u017eiadne nenakonfigurovan\u00e9 zariadenia.", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresa Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/util.py b/homeassistant/components/led_ble/util.py deleted file mode 100644 index e43655e2905..00000000000 --- a/homeassistant/components/led_ble/util.py +++ /dev/null @@ -1,51 +0,0 @@ -"""The yalexs_ble integration models.""" -from __future__ import annotations - -from homeassistant.components.bluetooth import ( - BluetoothScanningMode, - BluetoothServiceInfoBleak, - async_discovered_service_info, - async_process_advertisements, -) -from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher -from homeassistant.core import HomeAssistant, callback - -from .const import DEVICE_TIMEOUT - - -@callback -def async_find_existing_service_info( - hass: HomeAssistant, local_name: str, address: str -) -> BluetoothServiceInfoBleak | None: - """Return the service info for the given local_name and address.""" - for service_info in async_discovered_service_info(hass): - device = service_info.device - if device.address == address: - return service_info - return None - - -async def async_get_service_info( - hass: HomeAssistant, local_name: str, address: str -) -> BluetoothServiceInfoBleak: - """Wait for the service info for the given local_name and address.""" - if service_info := async_find_existing_service_info(hass, local_name, address): - return service_info - return await async_process_advertisements( - hass, - lambda service_info: True, - BluetoothCallbackMatcher({ADDRESS: address}), - BluetoothScanningMode.ACTIVE, - DEVICE_TIMEOUT, - ) - - -def short_address(address: str) -> str: - """Convert a Bluetooth address to a short address.""" - split_address = address.replace("-", ":").split(":") - return f"{split_address[-2].upper()}{split_address[-1].upper()}"[-4:] - - -def human_readable_name(name: str | None, local_name: str, address: str) -> str: - """Return a human readable name for the given name, local_name, and address.""" - return f"{name or local_name} ({short_address(address)})" diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 6f3508e22eb..1af16a904d8 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -89,7 +89,6 @@ class LgTVDevice(MediaPlayerEntity): self._channel_id = None self._channel_name = "" self._program_name = "" - self._state = None self._sources = {} self._source_names = [] @@ -100,14 +99,14 @@ class LgTVDevice(MediaPlayerEntity): with self._client as client: client.send_command(command) except (LgNetCastError, RequestException): - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF def update(self) -> None: """Retrieve the latest data from the LG TV.""" try: with self._client as client: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self.__update_volume() @@ -142,7 +141,7 @@ class LgTVDevice(MediaPlayerEntity): ) self._source_names = [n for n, k in sorted_sources] except (LgNetCastError, RequestException): - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF def __update_volume(self): volume_info = self._client.get_volume() @@ -156,11 +155,6 @@ class LgTVDevice(MediaPlayerEntity): """Return the name of the device.""" return self._name - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def is_volume_muted(self): """Boolean if volume is currently muted.""" @@ -197,7 +191,7 @@ class LgTVDevice(MediaPlayerEntity): return self._program_name @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self._on_action_script: return SUPPORT_LGTV | MediaPlayerEntityFeature.TURN_ON @@ -249,13 +243,13 @@ class LgTVDevice(MediaPlayerEntity): def media_play(self) -> None: """Send play command.""" self._playing = True - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self.send_command(LG_COMMAND.PLAY) def media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED self.send_command(LG_COMMAND.PAUSE) def media_next_track(self) -> None: diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py index 0606bad2d67..d2cb1749689 100644 --- a/homeassistant/components/lg_soundbar/config_flow.py +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure the LG Soundbar integration.""" -from queue import Full, Queue +import logging +from queue import Empty, Full, Queue import socket import temescal @@ -7,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DOMAIN @@ -14,50 +16,64 @@ DATA_SCHEMA = { vol.Required(CONF_HOST): str, } +_LOGGER = logging.getLogger(__name__) + +QUEUE_TIMEOUT = 10 + def test_connect(host, port): """LG Soundbar config flow test_connect.""" uuid_q = Queue(maxsize=1) name_q = Queue(maxsize=1) + def check_msg_response(response, msgs, attr): + msg = response["msg"] + if msg == msgs or msg in msgs: + if "data" in response and attr in response["data"]: + return True + _LOGGER.debug( + "[%s] msg did not contain expected attr [%s]: %s", msg, attr, response + ) + return False + def queue_add(attr_q, data): try: attr_q.put_nowait(data) except Full: - pass + _LOGGER.debug("attempted to add [%s] to full queue", data) def msg_callback(response): - if ( - response["msg"] in ["MAC_INFO_DEV", "PRODUCT_INFO"] - and "s_uuid" in response["data"] - ): + if check_msg_response(response, ["MAC_INFO_DEV", "PRODUCT_INFO"], "s_uuid"): queue_add(uuid_q, response["data"]["s_uuid"]) - if ( - response["msg"] == "SPK_LIST_VIEW_INFO" - and "s_user_name" in response["data"] - ): + if check_msg_response(response, "SPK_LIST_VIEW_INFO", "s_user_name"): queue_add(name_q, response["data"]["s_user_name"]) + details = {} + try: connection = temescal.temescal(host, port=port, callback=msg_callback) + connection.get_info() connection.get_mac_info() if uuid_q.empty(): connection.get_product_info() - connection.get_info() - details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} - return details + details["name"] = name_q.get(timeout=QUEUE_TIMEOUT) + details["uuid"] = uuid_q.get(timeout=QUEUE_TIMEOUT) + except Empty: + pass except socket.timeout as err: raise ConnectionError(f"Connection timeout with server: {host}:{port}") from err except OSError as err: raise ConnectionError(f"Cannot resolve hostname: {host}") from err + return details + class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """LG Soundbar config flow.""" VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_form() @@ -70,13 +86,19 @@ class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except ConnectionError: errors["base"] = "cannot_connect" else: - await self.async_set_unique_id(details["uuid"]) - self._abort_if_unique_id_configured() - info = { - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: DEFAULT_PORT, - } - return self.async_create_entry(title=details["name"], data=info) + if len(details) != 0: + info = { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + } + if "uuid" in details: + unique_id = details["uuid"] + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + else: + self._async_abort_entries_match(info) + return self.async_create_entry(title=details["name"], data=info) + errors["base"] = "no_data" return self._show_form(errors) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index c4491a1d257..577b4a8811a 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -25,7 +25,7 @@ async def async_setup_entry( LGDevice( config_entry.data[CONF_HOST], config_entry.data[CONF_PORT], - config_entry.unique_id, + config_entry.unique_id or config_entry.entry_id, ) ] ) @@ -82,7 +82,7 @@ class LGDevice(MediaPlayerEntity): def handle_event(self, response): """Handle responses from the speakers.""" - data = response["data"] + data = response["data"] if "data" in response else {} if response["msg"] == "EQ_VIEW_INFO": if "i_bass" in data: self._bass = data["i_bass"] diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json index 52d57eda809..8c6a9909ff5 100644 --- a/homeassistant/components/lg_soundbar/strings.json +++ b/homeassistant/components/lg_soundbar/strings.json @@ -8,7 +8,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_data": "Device did not return any data required to an entry." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/lg_soundbar/translations/ar.json b/homeassistant/components/lg_soundbar/translations/ar.json index 3fc833f41f1..0c025d8ce76 100644 --- a/homeassistant/components/lg_soundbar/translations/ar.json +++ b/homeassistant/components/lg_soundbar/translations/ar.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0627\u0644\u062e\u062f\u0645\u0629 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647\u0627 \u0645\u0633\u0628\u0642\u0627", - "existing_instance_updated": "\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062d\u0627\u0644\u064a" + "already_configured": "\u0627\u0644\u062e\u062f\u0645\u0629 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647\u0627 \u0645\u0633\u0628\u0642\u0627" }, "error": { "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" diff --git a/homeassistant/components/lg_soundbar/translations/ca.json b/homeassistant/components/lg_soundbar/translations/ca.json index 3d1b6f3bc98..b49df6df970 100644 --- a/homeassistant/components/lg_soundbar/translations/ca.json +++ b/homeassistant/components/lg_soundbar/translations/ca.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", - "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." + "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" diff --git a/homeassistant/components/lg_soundbar/translations/de.json b/homeassistant/components/lg_soundbar/translations/de.json index b8458a653aa..4c1588bfcad 100644 --- a/homeassistant/components/lg_soundbar/translations/de.json +++ b/homeassistant/components/lg_soundbar/translations/de.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" diff --git a/homeassistant/components/lg_soundbar/translations/el.json b/homeassistant/components/lg_soundbar/translations/el.json index 7fa31f8fe9d..687f872ea14 100644 --- a/homeassistant/components/lg_soundbar/translations/el.json +++ b/homeassistant/components/lg_soundbar/translations/el.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "existing_instance_updated": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index cbf35dc2976..19f5027d470 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", - "existing_instance_updated": "Updated existing configuration." + "no_uuid": "Device missing unique identification required for discovery." }, "error": { "cannot_connect": "Failed to connect" @@ -15,4 +15,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/lg_soundbar/translations/es.json b/homeassistant/components/lg_soundbar/translations/es.json index dc78afa232b..b9b39a74d9d 100644 --- a/homeassistant/components/lg_soundbar/translations/es.json +++ b/homeassistant/components/lg_soundbar/translations/es.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", - "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/lg_soundbar/translations/et.json b/homeassistant/components/lg_soundbar/translations/et.json index f5c73126124..2ee852479c9 100644 --- a/homeassistant/components/lg_soundbar/translations/et.json +++ b/homeassistant/components/lg_soundbar/translations/et.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "existing_instance_updated": "V\u00e4rskendati olemasolevat konfiguratsiooni." + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { "cannot_connect": "\u00dchendamine nurjus" diff --git a/homeassistant/components/lg_soundbar/translations/fr.json b/homeassistant/components/lg_soundbar/translations/fr.json index 5f6977ed3ab..33580a8eae3 100644 --- a/homeassistant/components/lg_soundbar/translations/fr.json +++ b/homeassistant/components/lg_soundbar/translations/fr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion" diff --git a/homeassistant/components/lg_soundbar/translations/hu.json b/homeassistant/components/lg_soundbar/translations/hu.json index c39033d273a..382c4f2d8d3 100644 --- a/homeassistant/components/lg_soundbar/translations/hu.json +++ b/homeassistant/components/lg_soundbar/translations/hu.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "existing_instance_updated": "A megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 friss\u00edtve." + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json index 3f6d9ea8f81..c8236f5ec73 100644 --- a/homeassistant/components/lg_soundbar/translations/id.json +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi", - "existing_instance_updated": "Memperbarui konfigurasi yang ada." + "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/lg_soundbar/translations/it.json b/homeassistant/components/lg_soundbar/translations/it.json index 30a7b328038..9cb86e4ee5a 100644 --- a/homeassistant/components/lg_soundbar/translations/it.json +++ b/homeassistant/components/lg_soundbar/translations/it.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "existing_instance_updated": "Configurazione esistente aggiornata." + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { "cannot_connect": "Impossibile connettersi" diff --git a/homeassistant/components/lg_soundbar/translations/ja.json b/homeassistant/components/lg_soundbar/translations/ja.json index cd40cd88044..4be154a597a 100644 --- a/homeassistant/components/lg_soundbar/translations/ja.json +++ b/homeassistant/components/lg_soundbar/translations/ja.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "existing_instance_updated": "\u65e2\u5b58\u306e\u69cb\u6210\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/lg_soundbar/translations/nl.json b/homeassistant/components/lg_soundbar/translations/nl.json index 7345479d97a..e0c21697099 100644 --- a/homeassistant/components/lg_soundbar/translations/nl.json +++ b/homeassistant/components/lg_soundbar/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Dienst is al geconfigureerd", - "existing_instance_updated": "Bestaande configuratie bijgewerkt." + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/lg_soundbar/translations/no.json b/homeassistant/components/lg_soundbar/translations/no.json index 58d4c11916b..7457ffad8a4 100644 --- a/homeassistant/components/lg_soundbar/translations/no.json +++ b/homeassistant/components/lg_soundbar/translations/no.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", - "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." + "already_configured": "Enheten er allerede konfigurert" }, "error": { "cannot_connect": "Tilkobling mislyktes" diff --git a/homeassistant/components/lg_soundbar/translations/pl.json b/homeassistant/components/lg_soundbar/translations/pl.json index 4a6b3d077df..1b4e47469aa 100644 --- a/homeassistant/components/lg_soundbar/translations/pl.json +++ b/homeassistant/components/lg_soundbar/translations/pl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json index 60e047a8acf..fb9b1f4c79e 100644 --- a/homeassistant/components/lg_soundbar/translations/pt-BR.json +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar" diff --git a/homeassistant/components/lg_soundbar/translations/ru.json b/homeassistant/components/lg_soundbar/translations/ru.json index 38f8ad9f92a..2475f72e796 100644 --- a/homeassistant/components/lg_soundbar/translations/ru.json +++ b/homeassistant/components/lg_soundbar/translations/ru.json @@ -1,8 +1,7 @@ { "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.", - "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." + "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." diff --git a/homeassistant/components/lg_soundbar/translations/sk.json b/homeassistant/components/lg_soundbar/translations/sk.json new file mode 100644 index 00000000000..c8e33ffce5e --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/sv.json b/homeassistant/components/lg_soundbar/translations/sv.json index 9b8ff6ea1aa..d8dcec22289 100644 --- a/homeassistant/components/lg_soundbar/translations/sv.json +++ b/homeassistant/components/lg_soundbar/translations/sv.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", - "existing_instance_updated": "Uppdaterade existerande konfiguration." + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" }, "error": { "cannot_connect": "Det gick inte att ansluta." diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json index c80f5540643..181980a8917 100644 --- a/homeassistant/components/lg_soundbar/translations/tr.json +++ b/homeassistant/components/lg_soundbar/translations/tr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "existing_instance_updated": "Mevcut yap\u0131land\u0131rma g\u00fcncellendi." + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" diff --git a/homeassistant/components/lg_soundbar/translations/zh-Hant.json b/homeassistant/components/lg_soundbar/translations/zh-Hant.json index 8680a863901..7582133c92f 100644 --- a/homeassistant/components/lg_soundbar/translations/zh-Hant.json +++ b/homeassistant/components/lg_soundbar/translations/zh-Hant.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index eab24ef7e42..d9333470b00 100644 --- a/homeassistant/components/lidarr/manifest.json +++ b/homeassistant/components/lidarr/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["aiopyarr"] + "loggers": ["aiopyarr"], + "integration_type": "service" } diff --git a/homeassistant/components/lidarr/translations/bg.json b/homeassistant/components/lidarr/translations/bg.json index 040b54c06e1..ee48b108f6f 100644 --- a/homeassistant/components/lidarr/translations/bg.json +++ b/homeassistant/components/lidarr/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/lidarr/translations/ca.json b/homeassistant/components/lidarr/translations/ca.json index 9cc30d6f893..7718ae1939b 100644 --- a/homeassistant/components/lidarr/translations/ca.json +++ b/homeassistant/components/lidarr/translations/ca.json @@ -28,15 +28,5 @@ "description": "La clau API es pot recuperar autom\u00e0ticament si les credencials d'inici de sessi\u00f3 no s'han establert a l'aplicaci\u00f3.\nLa teva clau API es pot trobar a Configuraci\u00f3 ('Settings') > General, a la interf\u00edcie web de Lidarr." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Nombre m\u00e0xim de registres a mostrar a la cua i a desitjats", - "upcoming_days": "Nombre dies propers a mostrar al calendari" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/cs.json b/homeassistant/components/lidarr/translations/cs.json new file mode 100644 index 00000000000..6ef4d1eb7ac --- /dev/null +++ b/homeassistant/components/lidarr/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "zeroconf_failed": "Kl\u00ed\u010d API nebyl nalezen. Zadejte jej pros\u00edm ru\u010dn\u011b" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kl\u00ed\u010d API" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "url": "URL", + "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/de.json b/homeassistant/components/lidarr/translations/de.json index a51b9c24a2f..87282f13d10 100644 --- a/homeassistant/components/lidarr/translations/de.json +++ b/homeassistant/components/lidarr/translations/de.json @@ -16,7 +16,7 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Die Lidarr-Integration muss manuell erneut mit der Lidarr-API authentifiziert werden", + "description": "Die Lidarr Integration muss manuell erneut mit der Lidarr-API authentifiziert werden", "title": "Integration erneut authentifizieren" }, "user": { @@ -25,17 +25,7 @@ "url": "URL", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, - "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest du unter Einstellungen > Allgemein in der Lidarr-Web-Benutzeroberfl\u00e4che." - } - } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Anzahl der maximal anzuzeigenden Datens\u00e4tze f\u00fcr Gesucht und Warteschlange", - "upcoming_days": "Anzahl der kommenden Tage, die im Kalender angezeigt werden sollen" - } + "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest du unter Einstellungen \u2192 Allgemein in der Lidarr-Web-Benutzeroberfl\u00e4che." } } } diff --git a/homeassistant/components/lidarr/translations/el.json b/homeassistant/components/lidarr/translations/el.json index 01a54904034..c3ef8b46f64 100644 --- a/homeassistant/components/lidarr/translations/el.json +++ b/homeassistant/components/lidarr/translations/el.json @@ -28,15 +28,5 @@ "description": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b5\u03ac\u03bd \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.\n\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u0393\u03b5\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf Lidarr Web UI." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03c9\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ce\u03bd \u03b3\u03b9\u03b1 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03b8\u03c5\u03bc\u03b7\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03bf\u03c5\u03c1\u03ac", - "upcoming_days": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03b7\u03bc\u03b5\u03c1\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b7\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/en.json b/homeassistant/components/lidarr/translations/en.json index 0e0475d25cd..cdb21be7fb2 100644 --- a/homeassistant/components/lidarr/translations/en.json +++ b/homeassistant/components/lidarr/translations/en.json @@ -28,15 +28,5 @@ "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Number of maximum records to display on wanted and queue", - "upcoming_days": "Number of upcoming days to display on calendar" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/es.json b/homeassistant/components/lidarr/translations/es.json index 071ee1312ec..ea4f66f431d 100644 --- a/homeassistant/components/lidarr/translations/es.json +++ b/homeassistant/components/lidarr/translations/es.json @@ -28,15 +28,5 @@ "description": "La clave API se puede recuperar autom\u00e1ticamente si las credenciales de inicio de sesi\u00f3n no se configuraron en la aplicaci\u00f3n.\nTu clave API se puede encontrar en Configuraci\u00f3n > General en la IU web de Lidarr." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "N\u00famero m\u00e1ximo de registros para mostrar en b\u00fasqueda y cola", - "upcoming_days": "N\u00famero de pr\u00f3ximos d\u00edas para mostrar en el calendario" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/et.json b/homeassistant/components/lidarr/translations/et.json index 0a88c87659f..a28f5b8435b 100644 --- a/homeassistant/components/lidarr/translations/et.json +++ b/homeassistant/components/lidarr/translations/et.json @@ -28,15 +28,5 @@ "description": "API-v\u00f5tme saab automaatselt alla laadida, kui rakenduses pole sisselogimismandaate m\u00e4\u00e4ratud.\n API-v\u00f5tme leiate Lidarri veebikasutajaliidese jaotisest Seaded > \u00dcldine." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Soovitud ja j\u00e4rjekorras kuvatavate kirjete maksimaalne arv", - "upcoming_days": "Kalendris kuvatavate eelseisvate p\u00e4evade arv" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/fr.json b/homeassistant/components/lidarr/translations/fr.json index 9eb6bf92cd2..2e2c289800b 100644 --- a/homeassistant/components/lidarr/translations/fr.json +++ b/homeassistant/components/lidarr/translations/fr.json @@ -27,15 +27,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Nombre maximal d'enregistrements \u00e0 afficher sur la recherche et la file d'attente", - "upcoming_days": "Nombre de jours \u00e0 venir \u00e0 afficher sur le calendrier" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/hu.json b/homeassistant/components/lidarr/translations/hu.json index a47d23df43c..7981b025ce0 100644 --- a/homeassistant/components/lidarr/translations/hu.json +++ b/homeassistant/components/lidarr/translations/hu.json @@ -28,15 +28,5 @@ "description": "Az API-kulcs automatikusan lek\u00e9rhet\u0151, ha a bejelentkez\u00e9si hiteles\u00edt\u0151 adatok nem lettek be\u00e1ll\u00edtva az alkalmaz\u00e1sban.\nAz API-kulcs a Lidarr webes felhaszn\u00e1l\u00f3i fel\u00fclet Be\u00e1ll\u00edt\u00e1sok > \u00c1ltal\u00e1nos men\u00fcpontj\u00e1ban tal\u00e1lhat\u00f3." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "A keresett \u00e9s a v\u00e1r\u00f3list\u00e1n megjelen\u00edtend\u0151 maxim\u00e1lis rekordok sz\u00e1ma", - "upcoming_days": "A napt\u00e1rban megjelen\u00edtend\u0151 k\u00f6vetkez\u0151 napok sz\u00e1ma" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/id.json b/homeassistant/components/lidarr/translations/id.json index 1514a016dc1..6a38e128ffa 100644 --- a/homeassistant/components/lidarr/translations/id.json +++ b/homeassistant/components/lidarr/translations/id.json @@ -28,15 +28,5 @@ "description": "Kunci API dapat diambil secara otomatis jika kredensial login tidak diatur dalam aplikasi.\nKunci API Anda dapat ditemukan di Settings > General di antarmuka web Lidarr." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Jumlah data maksimum untuk ditampilkan pada wanted dan queue", - "upcoming_days": "Jumlah hari yang akan datang untuk ditampilkan pada kalender" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/it.json b/homeassistant/components/lidarr/translations/it.json index b040d3eeb00..45a621e036b 100644 --- a/homeassistant/components/lidarr/translations/it.json +++ b/homeassistant/components/lidarr/translations/it.json @@ -28,15 +28,5 @@ "description": "La chiave API pu\u00f2 essere recuperata automaticamente se le credenziali di accesso non sono state impostate nell'applicazione.\nLa tua chiave API pu\u00f2 essere trovata in Impostazioni > Generali nell'interfaccia utente web di Lidarr." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Numero massimo di record da visualizzare su ricercato e coda", - "upcoming_days": "Numero di giorni successivi da visualizzare sul calendario" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/no.json b/homeassistant/components/lidarr/translations/no.json index 23c63f562b5..d7a2094302c 100644 --- a/homeassistant/components/lidarr/translations/no.json +++ b/homeassistant/components/lidarr/translations/no.json @@ -28,15 +28,5 @@ "description": "API-n\u00f8kkel kan hentes automatisk hvis p\u00e5loggingsinformasjon ikke ble angitt i applikasjonen.\n API-n\u00f8kkelen din finner du i Innstillinger > Generelt i Lidarr Web UI." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Antall maksimale poster \u00e5 vise p\u00e5 \u00f8nsket og k\u00f8", - "upcoming_days": "Antall kommende dager som skal vises i kalenderen" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/pl.json b/homeassistant/components/lidarr/translations/pl.json index 33d0deee79b..f329eb9efe8 100644 --- a/homeassistant/components/lidarr/translations/pl.json +++ b/homeassistant/components/lidarr/translations/pl.json @@ -28,15 +28,5 @@ "description": "Klucz API mo\u017ce zosta\u0107 pobrany automatycznie, je\u015bli dane logowania nie zosta\u0142y ustawione w aplikacji.\nTw\u00f3j klucz API mo\u017cesz znale\u017a\u0107 w Ustawienia > Og\u00f3lne, na swoim koncie Lidarr." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Maksymalna liczba wpis\u00f3w do wy\u015bwietlenia w poszukiwanych i w kolejce", - "upcoming_days": "Liczba nadchodz\u0105cych dni do wy\u015bwietlenia w kalendarzu" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/pt-BR.json b/homeassistant/components/lidarr/translations/pt-BR.json index 5d9b99704c4..6a46d2d7f78 100644 --- a/homeassistant/components/lidarr/translations/pt-BR.json +++ b/homeassistant/components/lidarr/translations/pt-BR.json @@ -28,15 +28,5 @@ "description": "A chave de API pode ser recuperada automaticamente se as credenciais de login n\u00e3o tiverem sido definidas no aplicativo.\n Sua chave de API pode ser encontrada em Configura\u00e7\u00f5es > Geral na IU da Web do Lidarr." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "N\u00famero m\u00e1ximo de registros a serem exibidos em desejados e em fila", - "upcoming_days": "N\u00famero de pr\u00f3ximos dias a serem exibidos no calend\u00e1rio" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/ru.json b/homeassistant/components/lidarr/translations/ru.json index afda2835228..cf319489331 100644 --- a/homeassistant/components/lidarr/translations/ru.json +++ b/homeassistant/components/lidarr/translations/ru.json @@ -28,15 +28,5 @@ "description": "\u041a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0435\u0441\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0435 \u0431\u044b\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u044b \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438.\n\u0412\u0430\u0448 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u00ab\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435\u00bb \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Lidarr." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u043f\u043e\u0438\u0441\u043a\u0435 \u0438 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u0438", - "upcoming_days": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438\u0445 \u0434\u043d\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/sk.json b/homeassistant/components/lidarr/translations/sk.json new file mode 100644 index 00000000000..824b22a1b5d --- /dev/null +++ b/homeassistant/components/lidarr/translations/sk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", + "wrong_app": "Dosiahnut\u00e1 nespr\u00e1vna aplik\u00e1cia. Sk\u00faste to pros\u00edm znova", + "zeroconf_failed": "K\u013e\u00fa\u010d API sa nena\u0161iel. Zadajte ho ru\u010dne" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + }, + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "url": "URL", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/sv.json b/homeassistant/components/lidarr/translations/sv.json index 6e87010feae..e5022336cb7 100644 --- a/homeassistant/components/lidarr/translations/sv.json +++ b/homeassistant/components/lidarr/translations/sv.json @@ -28,15 +28,5 @@ "description": "API-nyckel kan h\u00e4mtas automatiskt om inloggningsuppgifter inte st\u00e4llts in i applikationen.\n Din API-nyckel finns i Inst\u00e4llningar > Allm\u00e4nt i Lidarr Web UI." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Antal maximala poster att visa p\u00e5 \u00f6nskad och k\u00f6", - "upcoming_days": "Antal kommande dagar att visa i kalendern" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/tr.json b/homeassistant/components/lidarr/translations/tr.json index 39785efb9b0..2bc8e6239cf 100644 --- a/homeassistant/components/lidarr/translations/tr.json +++ b/homeassistant/components/lidarr/translations/tr.json @@ -28,15 +28,5 @@ "description": "Giri\u015f kimlik bilgileri uygulamada ayarlanmad\u0131ysa API anahtar\u0131 otomatik olarak al\u0131nabilir.\n API anahtar\u0131n\u0131z, Lidarr Web Kullan\u0131c\u0131 Aray\u00fcz\u00fcndeki Ayarlar > Genel b\u00f6l\u00fcm\u00fcnde bulunabilir." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Aranan ve kuyrukta g\u00f6r\u00fcnt\u00fclenecek maksimum kay\u0131t say\u0131s\u0131", - "upcoming_days": "Takvimde g\u00f6r\u00fcnt\u00fclenecek yakla\u015fan g\u00fcn say\u0131s\u0131" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/zh-Hant.json b/homeassistant/components/lidarr/translations/zh-Hant.json index d4d5b860b20..eaac487dfd3 100644 --- a/homeassistant/components/lidarr/translations/zh-Hant.json +++ b/homeassistant/components/lidarr/translations/zh-Hant.json @@ -28,15 +28,5 @@ "description": "\u5047\u5982\u6c92\u6709\u65bc\u61c9\u7528\u7a0b\u5f0f\u4e2d\u8a2d\u5b9a\u767b\u5165\u6191\u8b49\uff0c\u5247\u53ef\u4ee5\u81ea\u52d5\u53d6\u5f97 API \u91d1\u9470\u3002\n\u91d1\u9470\u53ef\u4ee5\u65bc Lidarr Web \u4ecb\u9762\u4e2d\u8a2d\u5b9a\uff08Settings\uff09 > \u4e00\u822c\uff08General\uff09\u4e2d\u53d6\u5f97\u3002" } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "\u986f\u793a\u60f3\u8981\u8207\u6392\u968a\u6700\u9ad8\u7d00\u9304\u6578\u76ee", - "upcoming_days": "\u5373\u5c07\u5230\u4f86\u884c\u4e8b\u66c6\u986f\u793a\u5929\u6578" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index a6ca0a16aa3..74ab63f88a1 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -6,8 +6,7 @@ from collections.abc import Mapping from contextlib import suppress from typing import Any, cast -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/life360/translations/bg.json b/homeassistant/components/life360/translations/bg.json index 22fad245c5d..7e4d5848bd3 100644 --- a/homeassistant/components/life360/translations/bg.json +++ b/homeassistant/components/life360/translations/bg.json @@ -3,15 +3,11 @@ "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" - }, - "create_entry": { - "default": "\u0417\u0430 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0440\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438, \u0432\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u043d\u0430 Life360]({docs_url})." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "invalid_username": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { @@ -26,7 +22,6 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, - "description": "\u0417\u0430 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0440\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438, \u0432\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u043d\u0430 Life360]({docs_url}). \u041f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u043e \u0435 \u0434\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u0435 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0438.", "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 Life360 \u043f\u0440\u043e\u0444\u0438\u043b" } } @@ -36,7 +31,7 @@ "init": { "data": { "driving_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u0448\u043e\u0444\u0438\u0440\u0430\u043d\u0435", - "max_gps_accuracy": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 GPS \u0442\u043e\u0447\u043d\u043e\u0441\u0442 (\u043c\u0435\u0442\u0440\u0438)" + "max_gps_accuracy": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 \u0442\u043e\u0447\u043d\u043e\u0441\u0442 \u043d\u0430 GPS (\u043c\u0435\u0442\u0440\u0438)" }, "title": "\u041e\u043f\u0446\u0438\u0438 \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442\u0430" } diff --git a/homeassistant/components/life360/translations/ca.json b/homeassistant/components/life360/translations/ca.json index f6b7a081863..04f1ec16dfb 100644 --- a/homeassistant/components/life360/translations/ca.json +++ b/homeassistant/components/life360/translations/ca.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "El compte ja est\u00e0 configurat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", - "unknown": "Error inesperat" - }, - "create_entry": { - "default": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url})." + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "already_configured": "El compte ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "invalid_username": "Nom d'usuari incorrecte", "unknown": "Error inesperat" }, "step": { @@ -28,7 +23,6 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d'afegir cap compte.", "title": "Configuraci\u00f3 del compte Life360" } } diff --git a/homeassistant/components/life360/translations/cs.json b/homeassistant/components/life360/translations/cs.json index 89e4299178d..a490d12695d 100644 --- a/homeassistant/components/life360/translations/cs.json +++ b/homeassistant/components/life360/translations/cs.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "create_entry": { - "default": "Chcete-li nastavit pokro\u010dil\u00e9 mo\u017enosti, pod\u00edvejte se do [dokumentace Life360]({docs_url})." + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "already_configured": "\u00da\u010det je ji\u017e nastaven", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "invalid_username": "Neplatn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { @@ -28,7 +23,6 @@ "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, - "description": "Chcete-li nastavit pokro\u010dil\u00e9 mo\u017enosti, pod\u00edvejte se do [dokumentace Life360]({docs_url}). Mo\u017en\u00e1 to budete cht\u00edt ud\u011blat p\u0159ed p\u0159id\u00e1n\u00edm \u00fa\u010dtu.", "title": "Informace o \u00fa\u010dtu Life360" } } diff --git a/homeassistant/components/life360/translations/da.json b/homeassistant/components/life360/translations/da.json index 71ce5215f25..6f5fd5164cc 100644 --- a/homeassistant/components/life360/translations/da.json +++ b/homeassistant/components/life360/translations/da.json @@ -1,18 +1,11 @@ { "config": { - "create_entry": { - "default": "Hvis du vil angive avancerede indstillinger skal du se [Life360 dokumentation]({docs_url})." - }, - "error": { - "invalid_username": "Ugyldigt brugernavn" - }, "step": { "user": { "data": { "password": "Adgangskode", "username": "Brugernavn" }, - "description": "Hvis du vil angive avancerede indstillinger skal du se [Life360 dokumentation]({docs_url}).\nDu \u00f8nsker m\u00e5ske at g\u00f8re dette f\u00f8r du tilf\u00f8jer konti.", "title": "Life360-kontooplysninger" } } diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 9e6e819a179..e084616c413 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Konto wurde bereits konfiguriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "unknown": "Unerwarteter Fehler" - }, - "create_entry": { - "default": "M\u00f6gliche erweiterte Einstellungen finden sich unter [Life360-Dokumentation]({docs_url})." + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "already_configured": "Konto wurde bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "invalid_username": "Ung\u00fcltiger Benutzername", "unknown": "Unerwarteter Fehler" }, "step": { @@ -28,7 +23,6 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Erweiterte Optionen sind in der [Life360-Dokumentation]({docs_url}) zu finden.\nDies sollte vor dem Hinzuf\u00fcgen von Kontoinformationen getan werden.", "title": "Life360-Konto konfigurieren" } } diff --git a/homeassistant/components/life360/translations/el.json b/homeassistant/components/life360/translations/el.json index f0db8e10ed6..b9105c05200 100644 --- a/homeassistant/components/life360/translations/el.json +++ b/homeassistant/components/life360/translations/el.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "create_entry": { - "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 Life360]({docs_url})." + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "invalid_username": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { @@ -28,7 +23,6 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 Life360]({docs_url}).\n\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c0\u03c1\u03b9\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd\u03c2.", "title": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" } } diff --git a/homeassistant/components/life360/translations/en.json b/homeassistant/components/life360/translations/en.json index 4e7ff35c814..5547b2d16ef 100644 --- a/homeassistant/components/life360/translations/en.json +++ b/homeassistant/components/life360/translations/en.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Account is already configured", "invalid_auth": "Invalid authentication", - "reauth_successful": "Re-authentication was successful", - "unknown": "Unexpected error" - }, - "create_entry": { - "default": "To set advanced options, see [Life360 documentation]({docs_url})." + "reauth_successful": "Re-authentication was successful" }, "error": { "already_configured": "Account is already configured", "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "invalid_username": "Invalid username", "unknown": "Unexpected error" }, "step": { @@ -28,7 +23,6 @@ "password": "Password", "username": "Username" }, - "description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts.", "title": "Configure Life360 Account" } } diff --git a/homeassistant/components/life360/translations/es-419.json b/homeassistant/components/life360/translations/es-419.json index 29b62e160fd..6a6f46181d6 100644 --- a/homeassistant/components/life360/translations/es-419.json +++ b/homeassistant/components/life360/translations/es-419.json @@ -1,18 +1,11 @@ { "config": { - "create_entry": { - "default": "Para establecer opciones avanzadas, consulte [Documentaci\u00f3n de Life360] ({docs_url})." - }, - "error": { - "invalid_username": "Nombre de usuario inv\u00e1lido" - }, "step": { "user": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Para establecer opciones avanzadas, consulte [Documentaci\u00f3n de Life360] ( {docs_url} ). \n Es posible que desee hacer eso antes de agregar cuentas.", "title": "Informaci\u00f3n de la cuenta Life360" } } diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index a9b53fffd86..c73c30de627 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", - "unknown": "Error inesperado" - }, - "create_entry": { - "default": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url})." + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "already_configured": "La cuenta ya est\u00e1 configurada", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_username": "Nombre de usuario no v\u00e1lido", "unknown": "Error inesperado" }, "step": { @@ -28,7 +23,6 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url}).\nEs posible que quieras hacerlo antes de a\u00f1adir cuentas.", "title": "Configurar cuenta de Life360" } } diff --git a/homeassistant/components/life360/translations/et.json b/homeassistant/components/life360/translations/et.json index 360aa8275f7..84ac7cc0617 100644 --- a/homeassistant/components/life360/translations/et.json +++ b/homeassistant/components/life360/translations/et.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "invalid_auth": "Tuvastamise viga", - "reauth_successful": "Taastuvastamine \u00f5nnestus", - "unknown": "Ootamatu t\u00f5rge" - }, - "create_entry": { - "default": "T\u00e4psemate suvandite kohta leiad teemat [Life360 documentation]({docs_url})." + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "already_configured": "Kasutaja on juba seadistatud", "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga", - "invalid_username": "Vale kasutajanimi", "unknown": "Ootamatu t\u00f5rge" }, "step": { @@ -28,7 +23,6 @@ "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "T\u00e4psemate suvandite kohta leiad teemat [Life360 documentation]({docs_url}).\nTee seda enne uute kontode lisamist.", "title": "Seadista Life360 konto" } } diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index ce1fd3f7757..a872c9909a7 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "invalid_auth": "Authentification non valide", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", - "unknown": "Erreur inattendue" - }, - "create_entry": { - "default": "Pour d\u00e9finir les options avanc\u00e9es, voir [Documentation de Life360]( {docs_url} )." + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", - "invalid_username": "Nom d'utilisateur non valide", "unknown": "Erreur inattendue" }, "step": { @@ -28,7 +23,6 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Pour d\u00e9finir des options avanc\u00e9es, voir [Documentation Life360]({docs_url}).\nVous pouvez le faire avant d'ajouter des comptes.", "title": "Configuration du compte Life360" } } diff --git a/homeassistant/components/life360/translations/he.json b/homeassistant/components/life360/translations/he.json index e4998f86963..d10579282e6 100644 --- a/homeassistant/components/life360/translations/he.json +++ b/homeassistant/components/life360/translations/he.json @@ -3,14 +3,12 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "invalid_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/life360/translations/hr.json b/homeassistant/components/life360/translations/hr.json index bb4a0b4fcf9..addfc0cbe81 100644 --- a/homeassistant/components/life360/translations/hr.json +++ b/homeassistant/components/life360/translations/hr.json @@ -1,11 +1,5 @@ { "config": { - "create_entry": { - "default": "Da biste postavili napredne opcije, pogledajte [Life360 dokumentacija] ( {docs_url} )." - }, - "error": { - "invalid_username": "Neispravno korisni\u010dko ime" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 1eec6f50643..571f6525700 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "create_entry": { - "default": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd: [Life360 dokument\u00e1ci\u00f3]({docs_url})." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -28,7 +23,6 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd a [Life360 dokument\u00e1ci\u00f3]({docs_url}) c\u00edm\u0171 r\u00e9szt.\n \u00c9rdemes ezt megtenni a fi\u00f3kok hozz\u00e1ad\u00e1sa el\u0151tt.", "title": "Life360 fi\u00f3k be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json index dfcdf97f46f..466b5bff996 100644 --- a/homeassistant/components/life360/translations/id.json +++ b/homeassistant/components/life360/translations/id.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Akun sudah dikonfigurasi", "invalid_auth": "Autentikasi tidak valid", - "reauth_successful": "Autentikasi ulang berhasil", - "unknown": "Kesalahan yang tidak diharapkan" - }, - "create_entry": { - "default": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url})." + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "already_configured": "Akun sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "invalid_username": "Nama pengguna tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { @@ -28,7 +23,6 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url}).\nAnda mungkin ingin melakukannya sebelum menambahkan akun.", "title": "Konfigurasikan Akun Life360" } } diff --git a/homeassistant/components/life360/translations/it.json b/homeassistant/components/life360/translations/it.json index 4f139301274..179cb0b27dc 100644 --- a/homeassistant/components/life360/translations/it.json +++ b/homeassistant/components/life360/translations/it.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", "invalid_auth": "Autenticazione non valida", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", - "unknown": "Errore imprevisto" - }, - "create_entry": { - "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360]({docs_url})." + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "invalid_username": "Nome utente non valido", "unknown": "Errore imprevisto" }, "step": { @@ -28,7 +23,6 @@ "password": "Password", "username": "Nome utente" }, - "description": "Per impostare le opzioni avanzate, vedere [Documentazione di Life360]({docs_url}).\n\u00c8 consigliabile eseguire questa operazione prima di aggiungere gli account.", "title": "Configura l'account Life360" } } diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index ab320748086..e3b07855deb 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "create_entry": { - "default": "\u8a73\u7d30\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001[Life360\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { @@ -28,7 +23,6 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "\u8a73\u7d30\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001[Life360\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8ffd\u52a0\u3059\u308b\u524d\u306b\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "Life360\u30a2\u30ab\u30a6\u30f3\u30c8\u60c5\u5831" } } diff --git a/homeassistant/components/life360/translations/ka.json b/homeassistant/components/life360/translations/ka.json index 35a27bfc78f..7aea64c3439 100644 --- a/homeassistant/components/life360/translations/ka.json +++ b/homeassistant/components/life360/translations/ka.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0", - "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0" + "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0" }, "error": { "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0", diff --git a/homeassistant/components/life360/translations/ko.json b/homeassistant/components/life360/translations/ko.json index d2ebd7c674f..eb00b434591 100644 --- a/homeassistant/components/life360/translations/ko.json +++ b/homeassistant/components/life360/translations/ko.json @@ -1,16 +1,11 @@ { "config": { "abort": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "create_entry": { - "default": "\uace0\uae09 \uc635\uc158\uc744 \uc124\uc815\ud558\ub824\uba74 [Life360 \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { @@ -19,7 +14,6 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "\uace0\uae09 \uc635\uc158\uc744 \uc124\uc815\ud558\ub824\uba74 [Life360 \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uacc4\uc815\uc744 \ucd94\uac00\ud558\uc2dc\uae30 \uc804\uc5d0 \uc77d\uc5b4\ubcf4\uc2dc\ub294\uac83\uc744 \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.", "title": "Life360 \uacc4\uc815 \uc815\ubcf4" } } diff --git a/homeassistant/components/life360/translations/lb.json b/homeassistant/components/life360/translations/lb.json index ef359f37810..a01eea8e330 100644 --- a/homeassistant/components/life360/translations/lb.json +++ b/homeassistant/components/life360/translations/lb.json @@ -1,16 +1,11 @@ { "config": { "abort": { - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "unknown": "Onerwaarte Feeler" - }, - "create_entry": { - "default": "Fir erweidert Optiounen anzestellen, kuckt [Life360 Dokumentatioun]({docs_url})." + "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, "error": { "already_configured": "Kont ass scho konfigur\u00e9iert", "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "invalid_username": "Ong\u00ebltege Benotzernumm", "unknown": "Onerwaarte Feeler" }, "step": { @@ -19,7 +14,6 @@ "password": "Passwuert", "username": "Benotzernumm" }, - "description": "Fir erweidert Optiounen anzestellen, kuckt [Life360 Dokumentatioun]({docs_url}).\nMaacht dat am beschten ier dir Konte b\u00e4isetzt.", "title": "Life360 Kont Informatiounen" } } diff --git a/homeassistant/components/life360/translations/nb.json b/homeassistant/components/life360/translations/nb.json index d00b0b51267..a22f7eef3d6 100644 --- a/homeassistant/components/life360/translations/nb.json +++ b/homeassistant/components/life360/translations/nb.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "unknown": "Uventet feil" - }, "error": { "unknown": "Uventet feil" } diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index b0e54bde3c5..f818a43a73a 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Account is al geconfigureerd", "invalid_auth": "Ongeldige authenticatie", - "reauth_successful": "Herauthenticatie geslaagd", - "unknown": "Onverwachte fout" - }, - "create_entry": { - "default": "Om geavanceerde opties in te stellen, zie [Life360 documentatie]({docs_url})." + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "already_configured": "Account is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", - "invalid_username": "Ongeldige gebruikersnaam", "unknown": "Onverwachte fout" }, "step": { @@ -28,7 +23,6 @@ "password": "Wachtwoord", "username": "Gebruikersnaam" }, - "description": "Om geavanceerde opties in te stellen, zie [Life360 documentatie]({docs_url}).\nMisschien wilt u dat doen voordat u accounts toevoegt.", "title": "Life360-accountgegevens" } } diff --git a/homeassistant/components/life360/translations/no.json b/homeassistant/components/life360/translations/no.json index 5095ced59f0..b0c590ec700 100644 --- a/homeassistant/components/life360/translations/no.json +++ b/homeassistant/components/life360/translations/no.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "invalid_auth": "Ugyldig godkjenning", - "reauth_successful": "Re-autentisering var vellykket", - "unknown": "Uventet feil" - }, - "create_entry": { - "default": "For \u00e5 angi avanserte alternativer, se [Life360 dokumentasjon]({docs_url})." + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "already_configured": "Kontoen er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", - "invalid_username": "Ugyldig brukernavn", "unknown": "Uventet feil" }, "step": { @@ -28,7 +23,6 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "For \u00e5 angi avanserte alternativer, se [Life360 dokumentasjon]({docs_url}). \nDet kan hende du vil gj\u00f8re det f\u00f8r du legger til kontoer.", "title": "Konfigurer Life360-konto" } } diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json index 2b1c7138079..bf4d2457835 100644 --- a/homeassistant/components/life360/translations/pl.json +++ b/homeassistant/components/life360/translations/pl.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", "invalid_auth": "Niepoprawne uwierzytelnienie", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "create_entry": { - "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "already_configured": "Konto jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", - "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { @@ -28,7 +23,6 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url}). Mo\u017cesz to zrobi\u0107 przed dodaniem kont.", "title": "Konfiguracja konta Life360" } } diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 25e917f2578..2beaa127927 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "unknown": "Erro inesperado" - }, - "create_entry": { - "default": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url})." + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "already_configured": "A conta j\u00e1 foi configurada", "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", "unknown": "Erro inesperado" }, "step": { @@ -28,7 +23,6 @@ "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url}). \n Voc\u00ea pode querer fazer isso antes de adicionar contas.", "title": "Configurar conta Life360" } } diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index cc3b190458f..f0b8db82092 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -3,14 +3,12 @@ "abort": { "already_configured": "Conta j\u00e1 configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", - "unknown": "Erro inesperado" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "already_configured": "Conta j\u00e1 configurada", "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "invalid_username": "Nome de utilizador incorreto", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index c0cd51a72d4..7bd5d776a05 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -3,17 +3,12 @@ "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.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "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.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "create_entry": { - "default": "\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]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "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": { "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.", "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { @@ -28,7 +23,6 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\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]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Life360" } } diff --git a/homeassistant/components/life360/translations/sk.json b/homeassistant/components/life360/translations/sk.json index 2c3ed1dd930..da9c32711a1 100644 --- a/homeassistant/components/life360/translations/sk.json +++ b/homeassistant/components/life360/translations/sk.json @@ -1,10 +1,42 @@ { "config": { "abort": { - "invalid_auth": "Neplatn\u00e9 overenie" + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "invalid_auth": "Neplatn\u00e9 overenie", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Nakonfigurujte \u00fa\u010det Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving_speed": "R\u00fdchlos\u0165 jazdy", + "limit_gps_acc": "Obmedzenie presnosti GPS", + "max_gps_accuracy": "Maxim\u00e1lna presnos\u0165 GPS (v metroch)" + }, + "title": "Mo\u017enosti \u00fa\u010dtu" + } } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/sl.json b/homeassistant/components/life360/translations/sl.json index 354c8c2618a..81742418d4b 100644 --- a/homeassistant/components/life360/translations/sl.json +++ b/homeassistant/components/life360/translations/sl.json @@ -1,18 +1,11 @@ { "config": { - "create_entry": { - "default": "\u010ce \u017eelite nastaviti napredne mo\u017enosti, glejte [Life360 dokumentacija]({docs_url})." - }, - "error": { - "invalid_username": "Napa\u010dno uporabni\u0161ko ime" - }, "step": { "user": { "data": { "password": "Geslo", "username": "Uporabni\u0161ko ime" }, - "description": "\u010ce \u017eelite nastaviti napredne mo\u017enosti, glejte [Life360 dokumentacija]({docs_url}). \n To lahko storite pred dodajanjem ra\u010dunov.", "title": "Podatki ra\u010duna Life360" } } diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 9f9168abdd2..8b9a76589f9 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Konto har redan konfigurerats", "invalid_auth": "Ogiltig autentisering", - "reauth_successful": "\u00c5terautentisering lyckades", - "unknown": "Ov\u00e4ntat fel" - }, - "create_entry": { - "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "already_configured": "Konto har redan konfigurerats", "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", - "invalid_username": "Ogiltigt anv\u00e4ndarnmn", "unknown": "Ov\u00e4ntat fel" }, "step": { @@ -28,7 +23,6 @@ "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" }, - "description": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url}).\nDu kanske vill g\u00f6ra det innan du l\u00e4gger till konton.", "title": "Life360 kontoinformation" } } diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index 52b083b83fd..c38acc09134 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Hesap 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", - "unknown": "Beklenmeyen hata" - }, - "create_entry": { - "default": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n." + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131", "unknown": "Beklenmeyen hata" }, "step": { @@ -28,7 +23,6 @@ "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n.\n Bunu hesap eklemeden \u00f6nce yapmak isteyebilirsiniz.", "title": "Life360 Hesab\u0131n\u0131 Yap\u0131land\u0131r" } } diff --git a/homeassistant/components/life360/translations/uk.json b/homeassistant/components/life360/translations/uk.json index caecf494388..6f74c07cc19 100644 --- a/homeassistant/components/life360/translations/uk.json +++ b/homeassistant/components/life360/translations/uk.json @@ -1,16 +1,11 @@ { "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." + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." }, "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": { @@ -19,7 +14,6 @@ "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" } } diff --git a/homeassistant/components/life360/translations/zh-Hans.json b/homeassistant/components/life360/translations/zh-Hans.json index a429b31dd82..296f1dba295 100644 --- a/homeassistant/components/life360/translations/zh-Hans.json +++ b/homeassistant/components/life360/translations/zh-Hans.json @@ -4,8 +4,7 @@ "invalid_auth": "\u65e0\u6548\u7684\u8eab\u4efd\u9a8c\u8bc1" }, "error": { - "invalid_auth": "\u65e0\u6548\u7684\u8eab\u4efd\u9a8c\u8bc1", - "invalid_username": "\u65e0\u6548\u7684\u7528\u6237\u540d" + "invalid_auth": "\u65e0\u6548\u7684\u8eab\u4efd\u9a8c\u8bc1" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/zh-Hant.json b/homeassistant/components/life360/translations/zh-Hant.json index 55e55bb30c7..6fa1834ac41 100644 --- a/homeassistant/components/life360/translations/zh-Hant.json +++ b/homeassistant/components/life360/translations/zh-Hant.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "create_entry": { - "default": "\u6b32\u8a2d\u5b9a\u9032\u968e\u9078\u9805\uff0c\u8acb\u53c3\u95b1 [Life360 \u6587\u4ef6]({docs_url})\u3002" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "invalid_username": "\u4f7f\u7528\u8005\u540d\u7a31\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { @@ -28,7 +23,6 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u6b32\u8a2d\u5b9a\u9032\u968e\u9078\u9805\uff0c\u8acb\u53c3\u95b1 [Life360 \u6587\u4ef6]({docs_url})\u3002\n\u5efa\u8b70\u65bc\u65b0\u589e\u5e33\u865f\u524d\uff0c\u5148\u9032\u884c\u4e86\u89e3\u3002", "title": "\u8a2d\u5b9a Life360 \u5e33\u865f" } } diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 786ddd6abbf..abe35c0a0d8 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -17,9 +17,10 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_call_later, async_track_time_interval @@ -163,7 +164,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: We do not want the discovery task to block startup. """ - asyncio.create_task(discovery_manager.async_discovery()) + task = asyncio.create_task(discovery_manager.async_discovery()) + + @callback + def _async_stop(_: Event) -> None: + if not task.done(): + task.cancel() + + # Task must be shut down when home assistant is closing + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) # Let the system settle a bit before starting discovery # to reduce the risk we miss devices because the event diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index 4b2a5b0895e..fa83ff5c427 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -31,7 +31,7 @@ from .util import ( class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for tplink.""" + """Handle a config flow for LIFX.""" VERSION = 1 @@ -41,7 +41,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_device: Light | None = None async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: - """Handle discovery via dhcp.""" + """Handle discovery via DHCP.""" mac = discovery_info.macaddress host = discovery_info.ip hass = self.hass @@ -70,8 +70,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: - """Handle discovery.""" - _LOGGER.debug("async_step_integration_discovery %s", discovery_info) + """Handle LIFX UDP broadcast discovery.""" serial = discovery_info[CONF_SERIAL] host = discovery_info[CONF_HOST] await self.async_set_unique_id(formatted_serial(serial)) @@ -82,7 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, host: str, serial: str | None = None ) -> FlowResult: """Handle any discovery.""" - _LOGGER.debug("Discovery %s %s", host, serial) self._async_abort_entries_match({CONF_HOST: host}) self.context[CONF_HOST] = host if any( @@ -222,20 +220,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except socket.gaierror: return None device: Light = connection.device - device.get_hostfirmware() try: - message = await async_execute_lifx(device.get_color) + # get_hostfirmware required for MAC address offset + # get_version required for lifx_features() + # get_label required to log the name of the device + messages = await asyncio.gather( + *[ + async_execute_lifx(device.get_hostfirmware), + async_execute_lifx(device.get_version), + async_execute_lifx(device.get_label), + ] + ) except asyncio.TimeoutError: return None finally: connection.async_stop() if ( - lifx_features(device)["relays"] is True + messages is None + or len(messages) != 3 + or lifx_features(device)["relays"] is True or device.host_firmware_version is None ): return None # relays not supported # device.mac_addr is not the mac_address, its the serial number - device.mac_addr = serial or message.target_addr + device.mac_addr = serial or messages[0].target_addr await self.async_set_unique_id( formatted_serial(device.mac_addr), raise_on_progress=raise_on_progress ) diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index f91ed761e44..1eb80c0c5a2 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -42,15 +42,17 @@ SERVICE_EFFECT_MOVE = "effect_move" SERVICE_EFFECT_PULSE = "effect_pulse" SERVICE_EFFECT_STOP = "effect_stop" +ATTR_CHANGE = "change" +ATTR_CYCLES = "cycles" +ATTR_DIRECTION = "direction" +ATTR_PALETTE = "palette" +ATTR_PERIOD = "period" ATTR_POWER_OFF = "power_off" ATTR_POWER_ON = "power_on" -ATTR_PERIOD = "period" -ATTR_CYCLES = "cycles" -ATTR_SPREAD = "spread" -ATTR_CHANGE = "change" -ATTR_DIRECTION = "direction" +ATTR_SATURATION_MAX = "saturation_max" +ATTR_SATURATION_MIN = "saturation_min" ATTR_SPEED = "speed" -ATTR_PALETTE = "palette" +ATTR_SPREAD = "spread" EFFECT_FLAME = "FLAME" EFFECT_MORPH = "MORPH" @@ -72,8 +74,8 @@ EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGH PULSE_MODE_BLINK = "blink" PULSE_MODE_BREATHE = "breathe" PULSE_MODE_PING = "ping" -PULSE_MODE_STROBE = "strobe" PULSE_MODE_SOLID = "solid" +PULSE_MODE_STROBE = "strobe" PULSE_MODES = [ PULSE_MODE_BLINK, @@ -90,8 +92,8 @@ LIFX_EFFECT_SCHEMA = { LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( { **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) @@ -121,8 +123,10 @@ LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema( { **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, + ATTR_SATURATION_MAX: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)), + ATTR_SATURATION_MIN: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)), ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), @@ -345,8 +349,21 @@ class LIFXManager: elif service == SERVICE_EFFECT_COLORLOOP: brightness = None + saturation_max = None + saturation_min = None + if ATTR_BRIGHTNESS in kwargs: brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + elif ATTR_BRIGHTNESS_PCT in kwargs: + brightness = convert_8_to_16( + round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100) + ) + + if ATTR_SATURATION_MAX in kwargs: + saturation_max = int(kwargs[ATTR_SATURATION_MAX] / 100 * 65535) + + if ATTR_SATURATION_MIN in kwargs: + saturation_min = int(kwargs[ATTR_SATURATION_MIN] / 100 * 65535) effect = aiolifx_effects.EffectColorloop( power_on=kwargs.get(ATTR_POWER_ON), @@ -355,6 +372,8 @@ class LIFXManager: spread=kwargs.get(ATTR_SPREAD), transition=kwargs.get(ATTR_TRANSITION), brightness=brightness, + saturation_max=saturation_max, + saturation_min=saturation_min, ) await self.effects_conductor.start(effect, bulbs) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index fc5422757b9..c1e7cc56915 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", "requirements": [ - "aiolifx==0.8.6", + "aiolifx==0.8.7", "aiolifx_effects==0.3.0", "aiolifx_themes==0.2.0" ], diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index 976d4ff5623..6613bb6a329 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -79,12 +79,20 @@ effect_pulse: - "strobe" - "solid" brightness: - name: Brightness - description: Number indicating brightness of the temporary color. + name: Brightness value + description: Number indicating brightness of the temporary color, where 1 is the minimum brightness and 255 is the maximum brightness supported by the light. selector: number: - min: 0 + min: 1 max: 255 + brightness_pct: + name: Brightness + description: Percentage indicating the brightness of the temporary color, where 1 is the minimum brightness and 100 is the maximum brightness supported by the light. + selector: + number: + min: 1 + max: 100 + unit_of_measurement: "%" color_name: name: Color name description: A human readable color name. @@ -131,12 +139,38 @@ effect_colorloop: domain: light fields: brightness: - name: Brightness - description: Number indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. + name: Brightness value + description: Number indicating brightness of the color loop, where 1 is the minimum brightness and 255 is the maximum brightness supported by the light. selector: number: min: 0 max: 255 + brightness_pct: + name: Brightness + description: Percentage indicating the brightness of the color loop, where 1 is the minimum brightness and 100 is the maximum brightness supported by the light. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" + saturation_min: + name: Minimum saturation + description: Percentage indicating the minimum saturation of the colors in the loop. + default: 80 + selector: + number: + min: 1 + max: 100 + unit_of_measurement: "%" + saturation_max: + name: Maximum saturation + description: Percentage indicating the maximum saturation of the colors in the loop. + default: 100 + selector: + number: + min: 1 + max: 100 + unit_of_measurement: "%" period: name: Period description: Duration between color changes. diff --git a/homeassistant/components/lifx/translations/bg.json b/homeassistant/components/lifx/translations/bg.json index 056e965f723..068c07dbdca 100644 --- a/homeassistant/components/lifx/translations/bg.json +++ b/homeassistant/components/lifx/translations/bg.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 LIFX \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", - "single_instance_allowed": "\u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 LIFX." + "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 LIFX \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 LIFX?" - }, "discovery_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/ca.json b/homeassistant/components/lifx/translations/ca.json index 8d0efc4de8a..57c706e19df 100644 --- a/homeassistant/components/lifx/translations/ca.json +++ b/homeassistant/components/lifx/translations/ca.json @@ -3,17 +3,13 @@ "abort": { "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", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + "no_devices_found": "No s'han trobat dispositius a la xarxa" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Vols configurar LIFX?" - }, "discovery_confirm": { "description": "Vols configurar {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/cs.json b/homeassistant/components/lifx/translations/cs.json index 660884bc1e7..d533e7fffab 100644 --- a/homeassistant/components/lifx/translations/cs.json +++ b/homeassistant/components/lifx/translations/cs.json @@ -3,16 +3,12 @@ "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", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { - "confirm": { - "description": "Chcete nastavit LIFX?" - }, "discovery_confirm": { "description": "Chcete nastavit {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/da.json b/homeassistant/components/lifx/translations/da.json index 14fbf83cbed..b6767f48f39 100644 --- a/homeassistant/components/lifx/translations/da.json +++ b/homeassistant/components/lifx/translations/da.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Der blev ikke fundet nogen LIFX-enheder p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Kun en enkelt konfiguration af LIFX er mulig." - }, - "step": { - "confirm": { - "description": "Konfigurer LIFX?" - } + "no_devices_found": "Der blev ikke fundet nogen LIFX-enheder p\u00e5 netv\u00e6rket." } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index 82e37b39c8b..ae056f136d7 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -3,17 +3,13 @@ "abort": { "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", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "M\u00f6chtest du LIFX einrichten?" - }, "discovery_confirm": { "description": "M\u00f6chtest du {label} ({host}) {serial} einrichten?" }, diff --git a/homeassistant/components/lifx/translations/el.json b/homeassistant/components/lifx/translations/el.json index 4ebea49190d..51556cc2af2 100644 --- a/homeassistant/components/lifx/translations/el.json +++ b/homeassistant/components/lifx/translations/el.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", - "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf LIFX;" - }, "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {label} ({host}) {serial};" }, diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json index 1f7cf981f5d..119259457a7 100644 --- a/homeassistant/components/lifx/translations/en.json +++ b/homeassistant/components/lifx/translations/en.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "no_devices_found": "No devices found on the network", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "no_devices_found": "No devices found on the network" }, "error": { "cannot_connect": "Failed to connect" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Do you want to set up LIFX?" - }, "discovery_confirm": { "description": "Do you want to setup {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/es-419.json b/homeassistant/components/lifx/translations/es-419.json index 023cec6a6db..8eb34ef0e34 100644 --- a/homeassistant/components/lifx/translations/es-419.json +++ b/homeassistant/components/lifx/translations/es-419.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "No se han encontrado dispositivos LIFX en la red.", - "single_instance_allowed": "S\u00f3lo es posible una \u00fanica configuraci\u00f3n de LIFX." - }, - "step": { - "confirm": { - "description": "\u00bfDesea configurar LIFX?" - } + "no_devices_found": "No se han encontrado dispositivos LIFX en la red." } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/es.json b/homeassistant/components/lifx/translations/es.json index 6bc2249182e..227e9fb3cdd 100644 --- a/homeassistant/components/lifx/translations/es.json +++ b/homeassistant/components/lifx/translations/es.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "no_devices_found": "No se encontraron dispositivos en la red", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { "cannot_connect": "No se pudo conectar" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "\u00bfQuieres configurar LIFX?" - }, "discovery_confirm": { "description": "\u00bfQuieres configurar {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/et.json b/homeassistant/components/lifx/translations/et.json index 6d06cbb17ba..fe05f4044ba 100644 --- a/homeassistant/components/lifx/translations/et.json +++ b/homeassistant/components/lifx/translations/et.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine juba k\u00e4ib", - "no_devices_found": "V\u00f5rgust ei leitud seadmeid", - "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks sidumine." + "no_devices_found": "V\u00f5rgust ei leitud seadmeid" }, "error": { "cannot_connect": "\u00dchendamine nurjus" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Kas soovid seadistada LIFX-i?" - }, "discovery_confirm": { "description": "Kas seadistada {label} ( {host} ) {serial} ?" }, diff --git a/homeassistant/components/lifx/translations/fi.json b/homeassistant/components/lifx/translations/fi.json deleted file mode 100644 index a92bc699280..00000000000 --- a/homeassistant/components/lifx/translations/fi.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "confirm": { - "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 LIFX:n?" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/fr.json b/homeassistant/components/lifx/translations/fr.json index c3f0561b085..5bfbe3fa87d 100644 --- a/homeassistant/components/lifx/translations/fr.json +++ b/homeassistant/components/lifx/translations/fr.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" }, "error": { "cannot_connect": "\u00c9chec de connexion" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Voulez-vous configurer LIFX?" - }, "discovery_confirm": { "description": "Voulez-vous configurer {label} ({host}) {serial}\u00a0?" }, diff --git a/homeassistant/components/lifx/translations/he.json b/homeassistant/components/lifx/translations/he.json index 0ea2e6e551b..9237bf45294 100644 --- a/homeassistant/components/lifx/translations/he.json +++ b/homeassistant/components/lifx/translations/he.json @@ -3,8 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/lifx/translations/hr.json b/homeassistant/components/lifx/translations/hr.json new file mode 100644 index 00000000000..7aff376ef2a --- /dev/null +++ b/homeassistant/components/lifx/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json index 588d5932e10..048509a9952 100644 --- a/homeassistant/components/lifx/translations/hu.json +++ b/homeassistant/components/lifx/translations/hu.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: LIFX?" - }, "discovery_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/id.json b/homeassistant/components/lifx/translations/id.json index 8781581bb0f..b9b3bb59207 100644 --- a/homeassistant/components/lifx/translations/id.json +++ b/homeassistant/components/lifx/translations/id.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" }, "error": { "cannot_connect": "Gagal terhubung" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Ingin menyiapkan LIFX?" - }, "discovery_confirm": { "description": "Ingin menyiapkan {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json index 9e8c090ad0d..8f6172f79ae 100644 --- a/homeassistant/components/lifx/translations/it.json +++ b/homeassistant/components/lifx/translations/it.json @@ -3,17 +3,13 @@ "abort": { "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", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + "no_devices_found": "Nessun dispositivo trovato sulla rete" }, "error": { "cannot_connect": "Impossibile connettersi" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Vuoi configurare LIFX?" - }, "discovery_confirm": { "description": "Vuoi configurare {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json index c3b144222a4..6b67ea51e28 100644 --- a/homeassistant/components/lifx/translations/ja.json +++ b/homeassistant/components/lifx/translations/ja.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "LIFX\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" - }, "discovery_confirm": { "description": "{label} ({host}) {serial} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" }, diff --git a/homeassistant/components/lifx/translations/ko.json b/homeassistant/components/lifx/translations/ko.json index 4d388cbeda2..d50e5e705bb 100644 --- a/homeassistant/components/lifx/translations/ko.json +++ b/homeassistant/components/lifx/translations/ko.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "step": { - "confirm": { - "description": "LIFX\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - } + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/lb.json b/homeassistant/components/lifx/translations/lb.json index 5455195f822..47a897ef157 100644 --- a/homeassistant/components/lifx/translations/lb.json +++ b/homeassistant/components/lifx/translations/lb.json @@ -1,13 +1,7 @@ { "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 LIFX konfigur\u00e9iert ginn?" - } + "no_devices_found": "Keng Apparater am Netzwierk fonnt." } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/nl.json b/homeassistant/components/lifx/translations/nl.json index 51091fcd365..fc7feecd443 100644 --- a/homeassistant/components/lifx/translations/nl.json +++ b/homeassistant/components/lifx/translations/nl.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", - "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Wilt u LIFX instellen?" - }, "discovery_confirm": { "description": "Wilt u {label} ({host}) {serial} instellen?" }, diff --git a/homeassistant/components/lifx/translations/no.json b/homeassistant/components/lifx/translations/no.json index 49ff5dea624..00bcd009eed 100644 --- a/homeassistant/components/lifx/translations/no.json +++ b/homeassistant/components/lifx/translations/no.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" }, "error": { "cannot_connect": "Tilkobling mislyktes" }, "flow_title": "{label} ( {host} ) {serial}", "step": { - "confirm": { - "description": "\u00d8nsker du \u00e5 sette opp LIFX?" - }, "discovery_confirm": { "description": "Vil du sette opp {label} ( {host} ) {serial} ?" }, diff --git a/homeassistant/components/lifx/translations/pl.json b/homeassistant/components/lifx/translations/pl.json index 817867d7c62..9bf20bc1e40 100644 --- a/homeassistant/components/lifx/translations/pl.json +++ b/homeassistant/components/lifx/translations/pl.json @@ -3,17 +3,13 @@ "abort": { "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", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" - }, "discovery_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index 616f3f03cc8..3ae1087f327 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "no_devices_found": "Nenhum dispositivo encontrado na rede", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "error": { "cannot_connect": "Falha ao conectar" }, "flow_title": "{label} ( {host} ) {serial}", "step": { - "confirm": { - "description": "Voc\u00ea quer configurar o LIFX?" - }, "discovery_confirm": { "description": "Deseja configurar {label} ( {host} ) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/pt.json b/homeassistant/components/lifx/translations/pt.json index 594ac7dacc4..5d7fdf356ef 100644 --- a/homeassistant/components/lifx/translations/pt.json +++ b/homeassistant/components/lifx/translations/pt.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "step": { - "confirm": { - "description": "Deseja configurar o LIFX?" - } + "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede." } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/ro.json b/homeassistant/components/lifx/translations/ro.json index 56e9307a8b3..ce0856d2bff 100644 --- a/homeassistant/components/lifx/translations/ro.json +++ b/homeassistant/components/lifx/translations/ro.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Nu exist\u0103 dispozitive LIFX g\u0103site \u00een re\u021bea.", - "single_instance_allowed": "Doar o singur\u0103 configura\u021bie de LIFX este posibil\u0103." - }, - "step": { - "confirm": { - "description": "Dori\u021bi s\u0103 configura\u021bi LIFX?" - } + "no_devices_found": "Nu exist\u0103 dispozitive LIFX g\u0103site \u00een re\u021bea." } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/ru.json b/homeassistant/components/lifx/translations/ru.json index 9e9a9460e19..cea461cb10d 100644 --- a/homeassistant/components/lifx/translations/ru.json +++ b/homeassistant/components/lifx/translations/ru.json @@ -3,17 +3,13 @@ "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.", "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.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + "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": { "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": "{label} ({host}) {serial}", "step": { - "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 LIFX?" - }, "discovery_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/sk.json b/homeassistant/components/lifx/translations/sk.json new file mode 100644 index 00000000000..f03ef39a4f3 --- /dev/null +++ b/homeassistant/components/lifx/translations/sk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{label} ({host}) {serial}", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Zariadenie" + } + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/sl.json b/homeassistant/components/lifx/translations/sl.json index dbbe051afd1..7047d591679 100644 --- a/homeassistant/components/lifx/translations/sl.json +++ b/homeassistant/components/lifx/translations/sl.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "V omre\u017eju ni najdenih naprav LIFX.", - "single_instance_allowed": "Mo\u017ena je samo ena konfiguracija LIFX-a." - }, - "step": { - "confirm": { - "description": "Ali \u017eelite nastaviti LIFX?" - } + "no_devices_found": "V omre\u017eju ni najdenih naprav LIFX." } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/sv.json b/homeassistant/components/lifx/translations/sv.json index dfd7de02d94..ba9248d4f4d 100644 --- a/homeassistant/components/lifx/translations/sv.json +++ b/homeassistant/components/lifx/translations/sv.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", - "no_devices_found": "Inga LIFX enheter hittas i n\u00e4tverket.", - "single_instance_allowed": "Endast en enda konfiguration av LIFX \u00e4r m\u00f6jlig." + "no_devices_found": "Inga LIFX enheter hittas i n\u00e4tverket." }, "error": { "cannot_connect": "Det gick inte att ansluta." }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Vill du st\u00e4lla in LIFX?" - }, "discovery_confirm": { "description": "Vill du st\u00e4lla in {label} ( {host} ) {serial} ?" }, diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json index 0f212e225be..a11798e6513 100644 --- a/homeassistant/components/lifx/translations/tr.json +++ b/homeassistant/components/lifx/translations/tr.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "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." + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "LIFX'i kurmak istiyor musunuz?" - }, "discovery_confirm": { "description": "{label} ( {host} ) {serial} kurmak istiyor musunuz?" }, diff --git a/homeassistant/components/lifx/translations/uk.json b/homeassistant/components/lifx/translations/uk.json index 556729e895b..1efd10692f9 100644 --- a/homeassistant/components/lifx/translations/uk.json +++ b/homeassistant/components/lifx/translations/uk.json @@ -1,13 +1,7 @@ { "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": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, - "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_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 newline at end of file diff --git a/homeassistant/components/lifx/translations/zh-Hans.json b/homeassistant/components/lifx/translations/zh-Hans.json index bf9b4277312..a501a8ace78 100644 --- a/homeassistant/components/lifx/translations/zh-Hans.json +++ b/homeassistant/components/lifx/translations/zh-Hans.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 LIFX \u8bbe\u5907\u3002", - "single_instance_allowed": "LIFX \u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" - }, - "step": { - "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e LIFX \u5417\uff1f" - } + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 LIFX \u8bbe\u5907\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index e8ff08be901..6fc7318a7b1 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -3,17 +3,13 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\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", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f" - }, "discovery_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {label} ({host}) {serial}\uff1f" }, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 5bf72b7267b..acc6252d3b3 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -5,7 +5,7 @@ from collections.abc import Iterable import csv import dataclasses from datetime import timedelta -from enum import IntEnum +from enum import IntFlag import logging import os from typing import Any, cast, final @@ -41,7 +41,7 @@ DATA_PROFILES = "light_profiles" ENTITY_ID_FORMAT = DOMAIN + ".{}" -class LightEntityFeature(IntEnum): +class LightEntityFeature(IntFlag): """Supported features of the light entity.""" EFFECT = 4 @@ -793,7 +793,7 @@ class LightEntity(ToggleEntity): _attr_rgbw_color: tuple[int, int, int, int] | None = None _attr_rgbww_color: tuple[int, int, int, int, int] | None = None _attr_supported_color_modes: set[ColorMode] | set[str] | None = None - _attr_supported_features: int = 0 + _attr_supported_features: LightEntityFeature = LightEntityFeature(0) _attr_xy_color: tuple[float, float] | None = None @property @@ -1060,6 +1060,6 @@ class LightEntity(ToggleEntity): return self._attr_supported_color_modes @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag supported features.""" return self._attr_supported_features diff --git a/homeassistant/components/light/translations/is.json b/homeassistant/components/light/translations/is.json index 365502c2032..9e4ab1e453f 100644 --- a/homeassistant/components/light/translations/is.json +++ b/homeassistant/components/light/translations/is.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} er sl\u00f6kkt", + "is_on": "{entity_name} er kveikt" + } + }, "state": { "_": { "off": "Sl\u00f6kkt", diff --git a/homeassistant/components/light/translations/sk.json b/homeassistant/components/light/translations/sk.json index 5294df79ce7..da643696125 100644 --- a/homeassistant/components/light/translations/sk.json +++ b/homeassistant/components/light/translations/sk.json @@ -1,4 +1,23 @@ { + "device_automation": { + "action_type": { + "brightness_decrease": "Zn\u00ed\u017ete jas {entity_name}", + "brightness_increase": "Zv\u00fd\u0161te jas {entity_name}", + "flash": "Flash {entity_name}", + "toggle": "Prepn\u00fa\u0165 {entity_name}", + "turn_off": "Vypn\u00fa\u0165 {entity_name}", + "turn_on": "Zapn\u00fa\u0165 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} je vypnut\u00e9", + "is_on": "{entity_name} je zapnut\u00e9" + }, + "trigger_type": { + "changed_states": "{entity_name} zapnut\u00e9 alebo vypnut\u00e9", + "turned_off": "{entity_name} vypnut\u00e1", + "turned_on": "{entity_name} zapnut\u00e1" + } + }, "state": { "_": { "off": "Vypnut\u00e9", diff --git a/homeassistant/components/litejet/translations/sk.json b/homeassistant/components/litejet/translations/sk.json index 892b8b2cd91..70783638575 100644 --- a/homeassistant/components/litejet/translations/sk.json +++ b/homeassistant/components/litejet/translations/sk.json @@ -1,10 +1,24 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "open_failed": "Nie je mo\u017en\u00e9 otvori\u0165 zadan\u00fd s\u00e9riov\u00fd port." + }, "step": { "user": { "data": { "port": "Port" - } + }, + "title": "Pripoji\u0165 sa k LiteJet" + } + } + }, + "options": { + "step": { + "init": { + "title": "Nakonfigurujte LiteJet" } } } diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 6384df2f25a..889c7edfd9c 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", - "loggers": ["pylitterbot"] + "loggers": ["pylitterbot"], + "integration_type": "hub" } diff --git a/homeassistant/components/litterrobot/translations/bg.json b/homeassistant/components/litterrobot/translations/bg.json index 7664989fae5..71ce9e1b4af 100644 --- a/homeassistant/components/litterrobot/translations/bg.json +++ b/homeassistant/components/litterrobot/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json index 8259aa1d16c..15f7a2107b2 100644 --- a/homeassistant/components/litterrobot/translations/de.json +++ b/homeassistant/components/litterrobot/translations/de.json @@ -27,8 +27,8 @@ }, "issues": { "migrated_attributes": { - "description": "Die Vakuumentit\u00e4tsattribute sind jetzt als Diagnosesensoren verf\u00fcgbar. \n\nBitte passe eventuell vorhandene Automatisierungen oder Skripte an, die diese Attribute verwenden.", - "title": "Litter-Robot-Attribute sind jetzt ihre eigenen Sensoren" + "description": "Die Staubsaugerentit\u00e4tsattribute sind jetzt als Diagnosesensoren verf\u00fcgbar. \n\nBitte passe eventuell vorhandene Automatisierungen oder Skripte an, die diese Attribute verwenden.", + "title": "Litter-Robot Attribute sind jetzt ihre eigenen Sensoren" } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.sk.json b/homeassistant/components/litterrobot/translations/sensor.sk.json index b4c5c43292d..4b9941a9477 100644 --- a/homeassistant/components/litterrobot/translations/sensor.sk.json +++ b/homeassistant/components/litterrobot/translations/sensor.sk.json @@ -1,7 +1,10 @@ { "state": { "litterrobot__status_code": { + "cd": "Zisten\u00e1 ma\u010dka", "off": "Vypnut\u00fd", + "offline": "Offline", + "p": "Pozastaven\u00fd", "rdy": "Pripraven\u00fd" } } diff --git a/homeassistant/components/litterrobot/translations/sk.json b/homeassistant/components/litterrobot/translations/sk.json index 5ada995aa6e..3896b31534b 100644 --- a/homeassistant/components/litterrobot/translations/sk.json +++ b/homeassistant/components/litterrobot/translations/sk.json @@ -1,7 +1,28 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Aktualizujte svoje heslo pre {username}", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/livisi/__init__.py b/homeassistant/components/livisi/__init__.py new file mode 100644 index 00000000000..38e0c9f8e7d --- /dev/null +++ b/homeassistant/components/livisi/__init__.py @@ -0,0 +1,57 @@ +"""The Livisi Smart Home integration.""" +from __future__ import annotations + +import asyncio +from typing import Final + +from aiohttp import ClientConnectorError +from aiolivisi import AioLivisi + +from homeassistant import core +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, device_registry as dr + +from .const import DOMAIN, SWITCH_PLATFORM +from .coordinator import LivisiDataUpdateCoordinator + +PLATFORMS: Final = [SWITCH_PLATFORM] + + +async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Livisi Smart Home from a config entry.""" + web_session = aiohttp_client.async_get_clientsession(hass) + aiolivisi = AioLivisi(web_session) + coordinator = LivisiDataUpdateCoordinator(hass, entry, aiolivisi) + try: + await coordinator.async_setup() + await coordinator.async_set_all_rooms() + except ClientConnectorError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=coordinator.serial_number, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Livisi", + name=f"SHC {coordinator.controller_type} {coordinator.serial_number}", + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await coordinator.async_config_entry_first_refresh() + asyncio.create_task(coordinator.ws_connect()) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + unload_success = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + await coordinator.websocket.disconnect() + if unload_success: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_success diff --git a/homeassistant/components/livisi/config_flow.py b/homeassistant/components/livisi/config_flow.py new file mode 100644 index 00000000000..16cccaacfd1 --- /dev/null +++ b/homeassistant/components/livisi/config_flow.py @@ -0,0 +1,88 @@ +"""Config flow for Livisi Home Assistant.""" +from __future__ import annotations + +from contextlib import suppress +from typing import Any + +from aiohttp import ClientConnectorError +from aiolivisi import AioLivisi, errors as livisi_errors +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client + +from .const import CONF_HOST, CONF_PASSWORD, DOMAIN, LOGGER + + +class LivisiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Livisi Smart Home config flow.""" + + def __init__(self) -> None: + """Create the configuration file.""" + self.aio_livisi: AioLivisi = None + self.data_schema = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=self.data_schema) + + errors = {} + try: + await self._login(user_input) + except livisi_errors.WrongCredentialException: + errors["base"] = "wrong_password" + except livisi_errors.ShcUnreachableException: + errors["base"] = "cannot_connect" + except livisi_errors.IncorrectIpAddressException: + errors["base"] = "wrong_ip_address" + else: + controller_info: dict[str, Any] = {} + with suppress(ClientConnectorError): + controller_info = await self.aio_livisi.async_get_controller() + if controller_info: + return await self.create_entity(user_input, controller_info) + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", data_schema=self.data_schema, errors=errors + ) + + async def _login(self, user_input: dict[str, str]) -> None: + """Login into Livisi Smart Home.""" + web_session = aiohttp_client.async_get_clientsession(self.hass) + self.aio_livisi = AioLivisi(web_session) + livisi_connection_data = { + "ip_address": user_input[CONF_HOST], + "password": user_input[CONF_PASSWORD], + } + + await self.aio_livisi.async_set_token(livisi_connection_data) + + async def create_entity( + self, user_input: dict[str, str], controller_info: dict[str, Any] + ) -> FlowResult: + """Create LIVISI entity.""" + if (controller_data := controller_info.get("gateway")) is None: + controller_data = controller_info + controller_type = controller_data["controllerType"] + LOGGER.debug( + "Integrating SHC %s with serial number: %s", + controller_type, + controller_data["serialNumber"], + ) + + return self.async_create_entry( + title=f"SHC {controller_type}", + data={ + **user_input, + }, + ) diff --git a/homeassistant/components/livisi/const.py b/homeassistant/components/livisi/const.py new file mode 100644 index 00000000000..e6abc5118de --- /dev/null +++ b/homeassistant/components/livisi/const.py @@ -0,0 +1,18 @@ +"""Constants for the Livisi Smart Home integration.""" +import logging +from typing import Final + +LOGGER = logging.getLogger(__package__) +DOMAIN = "livisi" + +CONF_HOST = "host" +CONF_PASSWORD: Final = "password" +AVATAR_PORT: Final = 9090 +CLASSIC_PORT: Final = 8080 +DEVICE_POLLING_DELAY: Final = 60 +LIVISI_STATE_CHANGE: Final = "livisi_state_change" +LIVISI_REACHABILITY_CHANGE: Final = "livisi_reachability_change" + +SWITCH_PLATFORM: Final = "switch" + +PSS_DEVICE_TYPE: Final = "PSS" diff --git a/homeassistant/components/livisi/coordinator.py b/homeassistant/components/livisi/coordinator.py new file mode 100644 index 00000000000..70640c260fb --- /dev/null +++ b/homeassistant/components/livisi/coordinator.py @@ -0,0 +1,132 @@ +"""Code to manage fetching LIVISI data API.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from aiohttp import ClientConnectorError +from aiolivisi import AioLivisi, LivisiEvent, Websocket + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + AVATAR_PORT, + CLASSIC_PORT, + CONF_HOST, + CONF_PASSWORD, + DEVICE_POLLING_DELAY, + LIVISI_REACHABILITY_CHANGE, + LIVISI_STATE_CHANGE, + LOGGER, +) + + +class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): + """Class to manage fetching LIVISI data API.""" + + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, aiolivisi: AioLivisi + ) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + LOGGER, + name="Livisi devices", + update_interval=timedelta(seconds=DEVICE_POLLING_DELAY), + ) + self.config_entry = config_entry + self.hass = hass + self.aiolivisi = aiolivisi + self.websocket = Websocket(aiolivisi) + self.devices: set[str] = set() + self.rooms: dict[str, Any] = {} + self.serial_number: str = "" + self.controller_type: str = "" + self.is_avatar: bool = False + self.port: int = 0 + + async def _async_update_data(self) -> list[dict[str, Any]]: + """Get device configuration from LIVISI.""" + try: + return await self.async_get_devices() + except ClientConnectorError as exc: + raise UpdateFailed("Failed to get LIVISI the devices") from exc + + async def async_setup(self) -> None: + """Set up the Livisi Smart Home Controller.""" + if not self.aiolivisi.livisi_connection_data: + livisi_connection_data = { + "ip_address": self.config_entry.data[CONF_HOST], + "password": self.config_entry.data[CONF_PASSWORD], + } + + await self.aiolivisi.async_set_token( + livisi_connection_data=livisi_connection_data + ) + controller_data = await self.aiolivisi.async_get_controller() + if controller_data["controllerType"] == "Avatar": + self.port = AVATAR_PORT + self.is_avatar = True + else: + self.port = CLASSIC_PORT + self.is_avatar = False + self.serial_number = controller_data["serialNumber"] + self.controller_type = controller_data["controllerType"] + + async def async_get_devices(self) -> list[dict[str, Any]]: + """Set the discovered devices list.""" + return await self.aiolivisi.async_get_devices() + + async def async_get_pss_state(self, capability: str) -> bool | None: + """Set the PSS state.""" + response: dict[str, Any] = await self.aiolivisi.async_get_pss_state( + capability[1:] + ) + if response is None: + return None + on_state = response["onState"] + return on_state["value"] + + async def async_set_all_rooms(self) -> None: + """Set the room list.""" + response: list[dict[str, Any]] = await self.aiolivisi.async_get_all_rooms() + + for available_room in response: + available_room_config: dict[str, Any] = available_room["config"] + self.rooms[available_room["id"]] = available_room_config["name"] + + def on_data(self, event_data: LivisiEvent) -> None: + """Define a handler to fire when the data is received.""" + if event_data.onState is not None: + async_dispatcher_send( + self.hass, + f"{LIVISI_STATE_CHANGE}_{event_data.source}", + event_data.onState, + ) + if event_data.isReachable is not None: + async_dispatcher_send( + self.hass, + f"{LIVISI_REACHABILITY_CHANGE}_{event_data.source}", + event_data.isReachable, + ) + + async def on_close(self) -> None: + """Define a handler to fire when the websocket is closed.""" + for device_id in self.devices: + is_reachable: bool = False + async_dispatcher_send( + self.hass, + f"{LIVISI_REACHABILITY_CHANGE}_{device_id}", + is_reachable, + ) + + await self.websocket.connect(self.on_data, self.on_close, self.port) + + async def ws_connect(self) -> None: + """Connect the websocket.""" + await self.websocket.connect(self.on_data, self.on_close, self.port) diff --git a/homeassistant/components/livisi/manifest.json b/homeassistant/components/livisi/manifest.json new file mode 100644 index 00000000000..83045d9eb60 --- /dev/null +++ b/homeassistant/components/livisi/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "livisi", + "name": "LIVISI Smart Home", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/livisi", + "requirements": ["aiolivisi==0.0.14"], + "codeowners": ["@StefanIacobLivisi"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/livisi/strings.json b/homeassistant/components/livisi/strings.json new file mode 100644 index 00000000000..260ef07234b --- /dev/null +++ b/homeassistant/components/livisi/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "description": "Enter the IP address and the (local) password of the SHC.", + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "wrong_password": "The password is incorrect.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "wrong_ip_address": "The IP address is incorrect or the SHC cannot be reached locally." + } + } +} diff --git a/homeassistant/components/livisi/switch.py b/homeassistant/components/livisi/switch.py new file mode 100644 index 00000000000..bcb9a204411 --- /dev/null +++ b/homeassistant/components/livisi/switch.py @@ -0,0 +1,161 @@ +"""Code to handle a Livisi switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + DOMAIN, + LIVISI_REACHABILITY_CHANGE, + LIVISI_STATE_CHANGE, + LOGGER, + PSS_DEVICE_TYPE, +) +from .coordinator import LivisiDataUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switch device.""" + coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + @callback + def handle_coordinator_update() -> None: + """Add switch.""" + shc_devices: list[dict[str, Any]] = coordinator.data + entities: list[SwitchEntity] = [] + for device in shc_devices: + if ( + device["type"] == PSS_DEVICE_TYPE + and device["id"] not in coordinator.devices + ): + livisi_switch: SwitchEntity = create_entity( + config_entry, device, coordinator + ) + LOGGER.debug("Include device type: %s", device["type"]) + coordinator.devices.add(device["id"]) + entities.append(livisi_switch) + async_add_entities(entities) + + config_entry.async_on_unload( + coordinator.async_add_listener(handle_coordinator_update) + ) + + +def create_entity( + config_entry: ConfigEntry, + device: dict[str, Any], + coordinator: LivisiDataUpdateCoordinator, +) -> SwitchEntity: + """Create Switch Entity.""" + config_details: dict[str, Any] = device["config"] + capabilities: list = device["capabilities"] + room_id: str = device["location"] + room_name: str = coordinator.rooms[room_id] + livisi_switch = LivisiSwitch( + config_entry, + coordinator, + unique_id=device["id"], + manufacturer=device["manufacturer"], + device_type=device["type"], + name=config_details["name"], + capability_id=capabilities[0], + room=room_name, + ) + return livisi_switch + + +class LivisiSwitch(CoordinatorEntity[LivisiDataUpdateCoordinator], SwitchEntity): + """Represents the Livisi Switch.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: LivisiDataUpdateCoordinator, + unique_id: str, + manufacturer: str, + device_type: str, + name: str, + capability_id: str, + room: str, + ) -> None: + """Initialize the Livisi Switch.""" + self.config_entry = config_entry + self._attr_unique_id = unique_id + self._attr_name = name + self._capability_id = capability_id + self.aio_livisi = coordinator.aiolivisi + self._attr_available = False + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=manufacturer, + model=device_type, + name=name, + suggested_area=room, + via_device=(DOMAIN, config_entry.entry_id), + ) + super().__init__(coordinator) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + response = await self.aio_livisi.async_pss_set_state( + self._capability_id, is_on=True + ) + if response is None: + self._attr_available = False + raise HomeAssistantError(f"Failed to turn on {self._attr_name}") + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + response = await self.aio_livisi.async_pss_set_state( + self._capability_id, is_on=False + ) + if response is None: + self._attr_available = False + raise HomeAssistantError(f"Failed to turn off {self._attr_name}") + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + response = await self.coordinator.async_get_pss_state(self._capability_id) + if response is None: + self._attr_is_on = False + self._attr_available = False + else: + self._attr_is_on = response + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{LIVISI_STATE_CHANGE}_{self._capability_id}", + self.update_states, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}", + self.update_reachability, + ) + ) + + @callback + def update_states(self, state: bool) -> None: + """Update the states of the switch device.""" + self._attr_is_on = state + self.async_write_ha_state() + + @callback + def update_reachability(self, is_reachable: bool) -> None: + """Update the reachability of the switch device.""" + self._attr_available = is_reachable + self.async_write_ha_state() diff --git a/homeassistant/components/livisi/translations/bg.json b/homeassistant/components/livisi/translations/bg.json new file mode 100644 index 00000000000..76c9066450f --- /dev/null +++ b/homeassistant/components/livisi/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "wrong_ip_address": "IP \u0430\u0434\u0440\u0435\u0441\u044a\u0442 \u0435 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u0438\u043b\u0438 SHC \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0434\u043e\u0441\u0442\u0438\u0433\u043d\u0430\u0442 \u043b\u043e\u043a\u0430\u043b\u043d\u043e.", + "wrong_password": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0435 \u0433\u0440\u0435\u0448\u043d\u0430." + }, + "step": { + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u0438 (\u043b\u043e\u043a\u0430\u043b\u043d\u0430\u0442\u0430) \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/ca.json b/homeassistant/components/livisi/translations/ca.json new file mode 100644 index 00000000000..e765c9199e0 --- /dev/null +++ b/homeassistant/components/livisi/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "wrong_ip_address": "L'adre\u00e7a IP \u00e9s incorrecta o no es pot connectar amb l'SHC localment.", + "wrong_password": "La contrasenya \u00e9s incorrecta." + }, + "step": { + "user": { + "data": { + "host": "Adre\u00e7a IP", + "password": "Contrasenya" + }, + "description": "Introdueix l'adre\u00e7a IP i la contrasenya (local) de l'SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/cs.json b/homeassistant/components/livisi/translations/cs.json new file mode 100644 index 00000000000..03bd102a2d4 --- /dev/null +++ b/homeassistant/components/livisi/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "wrong_password": "Heslo je nespr\u00e1vn\u00e9." + }, + "step": { + "user": { + "data": { + "host": "IP adresa", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/de.json b/homeassistant/components/livisi/translations/de.json new file mode 100644 index 00000000000..d85e61a13db --- /dev/null +++ b/homeassistant/components/livisi/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "wrong_ip_address": "Die IP-Adresse ist falsch oder der SHC ist lokal nicht erreichbar.", + "wrong_password": "Das Passwort ist falsch." + }, + "step": { + "user": { + "data": { + "host": "IP-Adresse", + "password": "Passwort" + }, + "description": "Gib die IP-Adresse und das (lokale) Passwort des SHC ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/el.json b/homeassistant/components/livisi/translations/el.json new file mode 100644 index 00000000000..f192cdf285a --- /dev/null +++ b/homeassistant/components/livisi/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "wrong_ip_address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf SHC.", + "wrong_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2." + }, + "step": { + "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd (\u03c4\u03bf\u03c0\u03b9\u03ba\u03cc) \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/en.json b/homeassistant/components/livisi/translations/en.json new file mode 100644 index 00000000000..d561f09dd06 --- /dev/null +++ b/homeassistant/components/livisi/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "wrong_ip_address": "The IP address is incorrect or the SHC cannot be reached locally.", + "wrong_password": "The password is incorrect." + }, + "step": { + "user": { + "data": { + "host": "IP Address", + "password": "Password" + }, + "description": "Enter the IP address and the (local) password of the SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/es.json b/homeassistant/components/livisi/translations/es.json new file mode 100644 index 00000000000..87a51cc79fa --- /dev/null +++ b/homeassistant/components/livisi/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "wrong_ip_address": "La direcci\u00f3n IP es incorrecta o no se puede acceder localmente al SHC.", + "wrong_password": "La contrase\u00f1a es incorrecta." + }, + "step": { + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a" + }, + "description": "Introduce la direcci\u00f3n IP y la contrase\u00f1a (local) del SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/et.json b/homeassistant/components/livisi/translations/et.json new file mode 100644 index 00000000000..07459d159f4 --- /dev/null +++ b/homeassistant/components/livisi/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "wrong_ip_address": "IP-aadress on vale v\u00f5i SHC-d ei ole v\u00f5imalik kohtv\u00f5rgus k\u00e4tte saada.", + "wrong_password": "Salas\u00f5na on vale." + }, + "step": { + "user": { + "data": { + "host": "IP aadress", + "password": "Salas\u00f5na" + }, + "description": "Sisesta SHC IP-aadress ja (kohalik) parool." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/fr.json b/homeassistant/components/livisi/translations/fr.json new file mode 100644 index 00000000000..7824796490a --- /dev/null +++ b/homeassistant/components/livisi/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "wrong_password": "Le mot de passe est erron\u00e9." + }, + "step": { + "user": { + "data": { + "host": "Adresse IP", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/hu.json b/homeassistant/components/livisi/translations/hu.json new file mode 100644 index 00000000000..d203bc3c7c4 --- /dev/null +++ b/homeassistant/components/livisi/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "wrong_ip_address": "Az IP-c\u00edm helytelen, vagy az SHC nem \u00e9rhet\u0151 el a helyi h\u00e1l\u00f3zatban.", + "wrong_password": "A jelsz\u00f3 helytelen." + }, + "step": { + "user": { + "data": { + "host": "IP c\u00edm", + "password": "Jelsz\u00f3" + }, + "description": "Adja meg az SHC helyi IP-c\u00edm\u00e9t \u00e9s jelszav\u00e1t." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/id.json b/homeassistant/components/livisi/translations/id.json new file mode 100644 index 00000000000..8be49b10d35 --- /dev/null +++ b/homeassistant/components/livisi/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "wrong_ip_address": "Alamat IP salah atau SHC tidak dapat dihubungi secara lokal.", + "wrong_password": "Kata sandi salah." + }, + "step": { + "user": { + "data": { + "host": "Alamat IP", + "password": "Kata Sandi" + }, + "description": "Masukkan alamat IP dan kata sandi (lokal) SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/it.json b/homeassistant/components/livisi/translations/it.json new file mode 100644 index 00000000000..aa39f1037a9 --- /dev/null +++ b/homeassistant/components/livisi/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "wrong_ip_address": "L'indirizzo IP non \u00e8 corretto o l'SHC non pu\u00f2 essere raggiunto localmente.", + "wrong_password": "La password non \u00e8 corretta." + }, + "step": { + "user": { + "data": { + "host": "Indirizzo IP", + "password": "Password" + }, + "description": "Immettere l'indirizzo IP e la password (locale) dell'SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/no.json b/homeassistant/components/livisi/translations/no.json new file mode 100644 index 00000000000..a121f5ba163 --- /dev/null +++ b/homeassistant/components/livisi/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "wrong_ip_address": "IP-adressen er feil eller SHC kan ikke n\u00e5s lokalt.", + "wrong_password": "Passordet er feil." + }, + "step": { + "user": { + "data": { + "host": "IP adresse", + "password": "Passord" + }, + "description": "Skriv inn IP-adressen og det (lokale) passordet til SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/pl.json b/homeassistant/components/livisi/translations/pl.json new file mode 100644 index 00000000000..70fd9de4d7d --- /dev/null +++ b/homeassistant/components/livisi/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "wrong_ip_address": "Adres IP jest nieprawid\u0142owy lub SHC nie jest dost\u0119pne lokalnie.", + "wrong_password": "Has\u0142o jest nieprawid\u0142owe." + }, + "step": { + "user": { + "data": { + "host": "Adres IP", + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a adres IP i (lokalne) has\u0142o SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/pt-BR.json b/homeassistant/components/livisi/translations/pt-BR.json new file mode 100644 index 00000000000..94145dafc78 --- /dev/null +++ b/homeassistant/components/livisi/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "wrong_ip_address": "O endere\u00e7o IP est\u00e1 incorreto ou o SHC n\u00e3o pode ser alcan\u00e7ado localmente.", + "wrong_password": "A senha est\u00e1 incorreta." + }, + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP", + "password": "Senha" + }, + "description": "Digite o endere\u00e7o IP e a senha (local) do SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/ru.json b/homeassistant/components/livisi/translations/ru.json new file mode 100644 index 00000000000..9fcbe468154 --- /dev/null +++ b/homeassistant/components/livisi/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.", + "wrong_ip_address": "IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0432\u0435\u0440\u0435\u043d \u0438\u043b\u0438 SHC \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e.", + "wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438 (\u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439) \u043f\u0430\u0440\u043e\u043b\u044c SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/sk.json b/homeassistant/components/livisi/translations/sk.json new file mode 100644 index 00000000000..805af52a823 --- /dev/null +++ b/homeassistant/components/livisi/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "wrong_ip_address": "IP adresa je nespr\u00e1vna alebo SHC nie je mo\u017en\u00e9 lok\u00e1lne dosiahnu\u0165.", + "wrong_password": "Heslo je nespr\u00e1vne." + }, + "step": { + "user": { + "data": { + "host": "IP adresa", + "password": "Heslo" + }, + "description": "Zadajte IP adresu a (miestne) heslo SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/zh-Hant.json b/homeassistant/components/livisi/translations/zh-Hant.json new file mode 100644 index 00000000000..b1bced211d4 --- /dev/null +++ b/homeassistant/components/livisi/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "wrong_ip_address": "IP \u4f4d\u5740\u932f\u8aa4\u6216 SHC \u7121\u6cd5\u900f\u904e\u672c\u5e95\u7aef\u627e\u5230\u88dd\u7f6e\u3002", + "wrong_password": "\u5bc6\u78bc\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "host": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc" + }, + "description": "\u8f38\u5165 IP \u4f4d\u5740\u53ca\u672c\u5730\u7aef SHC \u5bc6\u78bc\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_calendar/__init__.py b/homeassistant/components/local_calendar/__init__.py new file mode 100644 index 00000000000..33ad67cc81a --- /dev/null +++ b/homeassistant/components/local_calendar/__init__.py @@ -0,0 +1,41 @@ +"""The Local Calendar integration.""" +from __future__ import annotations + +import logging +from pathlib import Path + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.util import slugify + +from .const import CONF_CALENDAR_NAME, DOMAIN +from .store import LocalCalendarStore + +_LOGGER = logging.getLogger(__name__) + + +PLATFORMS: list[Platform] = [Platform.CALENDAR] + +STORAGE_PATH = ".storage/local_calendar.{key}.ics" + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Local Calendar from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + key = slugify(entry.data[CONF_CALENDAR_NAME]) + path = Path(hass.config.path(STORAGE_PATH.format(key=key))) + hass.data[DOMAIN][entry.entry_id] = LocalCalendarStore(hass, path) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py new file mode 100644 index 00000000000..79d16634883 --- /dev/null +++ b/homeassistant/components/local_calendar/calendar.py @@ -0,0 +1,149 @@ +"""Calendar platform for a Local Calendar.""" + +from __future__ import annotations + +from datetime import datetime +import logging +from typing import Any + +from ical.calendar import Calendar +from ical.calendar_stream import IcsCalendarStream +from ical.event import Event +from ical.store import EventStore +from ical.types import Range, Recur + +from homeassistant.components.calendar import ( + EVENT_DESCRIPTION, + EVENT_END, + EVENT_RRULE, + EVENT_START, + EVENT_SUMMARY, + CalendarEntity, + CalendarEntityFeature, + CalendarEvent, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util + +from .const import CONF_CALENDAR_NAME, DOMAIN +from .store import LocalCalendarStore + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the local calendar platform.""" + store = hass.data[DOMAIN][config_entry.entry_id] + ics = await store.async_load() + calendar = IcsCalendarStream.calendar_from_ics(ics) + + name = config_entry.data[CONF_CALENDAR_NAME] + entity = LocalCalendarEntity(store, calendar, name, unique_id=config_entry.entry_id) + async_add_entities([entity], True) + + +class LocalCalendarEntity(CalendarEntity): + """A calendar entity backed by a local iCalendar file.""" + + _attr_has_entity_name = True + _attr_supported_features = ( + CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT + ) + + def __init__( + self, + store: LocalCalendarStore, + calendar: Calendar, + name: str, + unique_id: str, + ) -> None: + """Initialize LocalCalendarEntity.""" + self._store = store + self._calendar = calendar + self._event: CalendarEvent | None = None + self._attr_name = name.capitalize() + self._attr_unique_id = unique_id + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + return self._event + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + events = self._calendar.timeline_tz(dt_util.DEFAULT_TIME_ZONE).overlapping( + dt_util.as_local(start_date), + dt_util.as_local(end_date), + ) + return [_get_calendar_event(event) for event in events] + + async def async_update(self) -> None: + """Update entity state with the next upcoming event.""" + events = self._calendar.timeline_tz(dt_util.DEFAULT_TIME_ZONE).active_after( + dt_util.now() + ) + if event := next(events, None): + self._event = _get_calendar_event(event) + else: + self._event = None + + async def _async_store(self) -> None: + """Persist the calendar to disk.""" + content = IcsCalendarStream.calendar_to_ics(self._calendar) + await self._store.async_store(content) + + async def async_create_event(self, **kwargs: Any) -> None: + """Add a new event to calendar.""" + event = Event.parse_obj( + { + EVENT_SUMMARY: kwargs[EVENT_SUMMARY], + EVENT_START: kwargs[EVENT_START], + EVENT_END: kwargs[EVENT_END], + EVENT_DESCRIPTION: kwargs.get(EVENT_DESCRIPTION), + } + ) + if rrule := kwargs.get(EVENT_RRULE): + event.rrule = Recur.from_rrule(rrule) + + EventStore(self._calendar).add(event) + await self._async_store() + await self.async_update_ha_state(force_refresh=True) + + async def async_delete_event( + self, + uid: str, + recurrence_id: str | None = None, + recurrence_range: str | None = None, + ) -> None: + """Delete an event on the calendar.""" + range_value: Range = Range.NONE + if recurrence_range == Range.THIS_AND_FUTURE: + range_value = Range.THIS_AND_FUTURE + EventStore(self._calendar).delete( + uid, + recurrence_id=recurrence_id, + recurrence_range=range_value, + ) + await self._async_store() + await self.async_update_ha_state(force_refresh=True) + + +def _get_calendar_event(event: Event) -> CalendarEvent: + """Return a CalendarEvent from an API event.""" + return CalendarEvent( + summary=event.summary, + start=event.start, + end=event.end, + description=event.description, + uid=event.uid, + rrule=event.rrule.as_rrule_str() if event.rrule else None, + recurrence_id=event.recurrence_id, + ) diff --git a/homeassistant/components/local_calendar/config_flow.py b/homeassistant/components/local_calendar/config_flow.py new file mode 100644 index 00000000000..2bde06820b6 --- /dev/null +++ b/homeassistant/components/local_calendar/config_flow.py @@ -0,0 +1,36 @@ +"""Config flow for Local Calendar integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_CALENDAR_NAME, DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_CALENDAR_NAME): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Local Calendar.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + return self.async_create_entry( + title=user_input[CONF_CALENDAR_NAME], data=user_input + ) diff --git a/homeassistant/components/local_calendar/const.py b/homeassistant/components/local_calendar/const.py new file mode 100644 index 00000000000..49cd5dc22a4 --- /dev/null +++ b/homeassistant/components/local_calendar/const.py @@ -0,0 +1,5 @@ +"""Constants for the Local Calendar integration.""" + +DOMAIN = "local_calendar" + +CONF_CALENDAR_NAME = "calendar_name" diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json new file mode 100644 index 00000000000..fa258c389ab --- /dev/null +++ b/homeassistant/components/local_calendar/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "local_calendar", + "name": "Local Calendar", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/local_calendar", + "requirements": ["ical==4.2.1"], + "codeowners": ["@allenporter"], + "iot_class": "local_polling", + "loggers": ["ical"] +} diff --git a/homeassistant/components/local_calendar/store.py b/homeassistant/components/local_calendar/store.py new file mode 100644 index 00000000000..3955717a066 --- /dev/null +++ b/homeassistant/components/local_calendar/store.py @@ -0,0 +1,38 @@ +"""Local storage for the Local Calendar integration.""" + +import asyncio +from pathlib import Path + +from homeassistant.core import HomeAssistant + +STORAGE_PATH = ".storage/{key}.ics" + + +class LocalCalendarStore: + """Local calendar storage.""" + + def __init__(self, hass: HomeAssistant, path: Path) -> None: + """Initialize LocalCalendarStore.""" + self._hass = hass + self._path = path + self._lock = asyncio.Lock() + + async def async_load(self) -> str: + """Load the calendar from disk.""" + async with self._lock: + return await self._hass.async_add_executor_job(self._load) + + def _load(self) -> str: + """Load the calendar from disk.""" + if not self._path.exists(): + return "" + return self._path.read_text() + + async def async_store(self, ics_content: str) -> None: + """Persist the calendar to storage.""" + async with self._lock: + await self._hass.async_add_executor_job(self._store, ics_content) + + def _store(self, ics_content: str) -> None: + """Persist the calendar to storage.""" + self._path.write_text(ics_content) diff --git a/homeassistant/components/local_calendar/strings.json b/homeassistant/components/local_calendar/strings.json new file mode 100644 index 00000000000..f49c92e5438 --- /dev/null +++ b/homeassistant/components/local_calendar/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "Please choose a name for your new calendar", + "data": { + "calendar_name": "Calendar Name" + } + } + } + } +} diff --git a/homeassistant/components/local_calendar/translations/en.json b/homeassistant/components/local_calendar/translations/en.json new file mode 100644 index 00000000000..4bceb75616a --- /dev/null +++ b/homeassistant/components/local_calendar/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "calendar_name": "Calendar Name" + }, + "description": "Please choose a name for your new calendar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py index 27bd5340d40..be708f5d8b9 100644 --- a/homeassistant/components/local_ip/config_flow.py +++ b/homeassistant/components/local_ip/config_flow.py @@ -12,8 +12,6 @@ from .const import DOMAIN class SimpleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for local_ip.""" - VERSION = 1 - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index 56c1fac7c8f..4c502895b3f 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -7,7 +7,7 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, SENSOR +from .const import SENSOR async def async_setup_entry( @@ -16,7 +16,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the platform from config_entry.""" - name = entry.data.get(CONF_NAME) or DOMAIN + name = entry.data.get(CONF_NAME) or "Local IP" async_add_entities([IPSensor(name)], True) diff --git a/homeassistant/components/local_ip/translations/sk.json b/homeassistant/components/local_ip/translations/sk.json new file mode 100644 index 00000000000..ccfaf6af209 --- /dev/null +++ b/homeassistant/components/local_ip/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?", + "title": "Lok\u00e1lna IP adresa" + } + } + }, + "title": "Lok\u00e1lna IP adresa" +} \ No newline at end of file diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index f8fa1671034..2ec1e7437de 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,6 +1,5 @@ """Support for the Locative platform.""" -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/locative/translations/sk.json b/homeassistant/components/locative/translations/sk.json new file mode 100644 index 00000000000..04cb32a1c4e --- /dev/null +++ b/homeassistant/components/locative/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index d241d57e128..5008fa0ca2b 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -from enum import IntEnum +from enum import IntFlag import functools as ft import logging from typing import Any, final @@ -48,7 +48,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) LOCK_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) -class LockEntityFeature(IntEnum): +class LockEntityFeature(IntFlag): """Supported features of the lock entity.""" OPEN = 1 @@ -112,6 +112,7 @@ class LockEntity(Entity): _attr_is_unlocking: bool | None = None _attr_is_jammed: bool | None = None _attr_state: None = None + _attr_supported_features: LockEntityFeature = LockEntityFeature(0) @property def changed_by(self) -> str | None: @@ -190,3 +191,8 @@ class LockEntity(Entity): if (locked := self.is_locked) is None: return None return STATE_LOCKED if locked else STATE_UNLOCKED + + @property + def supported_features(self) -> LockEntityFeature: + """Return the list of supported features.""" + return self._attr_supported_features diff --git a/homeassistant/components/lock/translations/is.json b/homeassistant/components/lock/translations/is.json index e1960ee5888..4de60d6af72 100644 --- a/homeassistant/components/lock/translations/is.json +++ b/homeassistant/components/lock/translations/is.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} er l\u00e6st", + "is_unlocked": "{entity_name} er \u00f3l\u00e6st" + } + }, "state": { "_": { "locked": "L\u00e6st", diff --git a/homeassistant/components/lock/translations/sk.json b/homeassistant/components/lock/translations/sk.json index c01a1106cd5..17c0e221f97 100644 --- a/homeassistant/components/lock/translations/sk.json +++ b/homeassistant/components/lock/translations/sk.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "lock": "Uzamkn\u00fa\u0165 {entity_name}", + "open": "Otvori\u0165 {entity_name}", + "unlock": "Odomkn\u00fa\u0165 {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} je uzamknut\u00fd", + "is_unlocked": "{entity_name} je odomknut\u00fd" + }, + "trigger_type": { + "locked": "{entity_name} uzamknut\u00fd", + "unlocked": "{entity_name} odomknut\u00fd" + } + }, "state": { "_": { "locked": "Zamknut\u00fd", diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index 5fc999d7d11..0d087ef23b7 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,5 +1,8 @@ """Support for setting the level of logging for components.""" +from __future__ import annotations + import logging +import re import voluptuous as vol @@ -7,29 +10,26 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -DOMAIN = "logger" +from . import websocket_api +from .const import ( + ATTR_LEVEL, + DEFAULT_LOGSEVERITY, + DOMAIN, + LOGGER_DEFAULT, + LOGGER_FILTERS, + LOGGER_LOGS, + LOGSEVERITY, + SERVICE_SET_DEFAULT_LEVEL, + SERVICE_SET_LEVEL, +) +from .helpers import ( + LoggerDomainConfig, + LoggerSettings, + set_default_log_level, + set_log_levels, +) -SERVICE_SET_DEFAULT_LEVEL = "set_default_level" -SERVICE_SET_LEVEL = "set_level" - -LOGSEVERITY = { - "CRITICAL": 50, - "FATAL": 50, - "ERROR": 40, - "WARNING": 30, - "WARN": 30, - "INFO": 20, - "DEBUG": 10, - "NOTSET": 0, -} - -LOGGER_DEFAULT = "default" -LOGGER_LOGS = "logs" -LOGGER_FILTERS = "filters" - -ATTR_LEVEL = "level" - -_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) +_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY), LOGSEVERITY.__getitem__) SERVICE_SET_DEFAULT_LEVEL_SCHEMA = vol.Schema({ATTR_LEVEL: _VALID_LOG_LEVEL}) SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL}) @@ -38,7 +38,9 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, + vol.Optional( + LOGGER_DEFAULT, default=DEFAULT_LOGSEVERITY + ): _VALID_LOG_LEVEL, vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}), vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}), } @@ -50,42 +52,38 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the logger component.""" - hass.data[DOMAIN] = {} - logging.setLoggerClass(_get_logger_class(hass.data[DOMAIN])) - @callback - def set_default_log_level(level): - """Set the default log level for components.""" - _set_log_level(logging.getLogger(""), level) + settings = LoggerSettings(hass, config) - @callback - def set_log_levels(logpoints): - """Set the specified log levels.""" - hass.data[DOMAIN].update(logpoints) - for key, value in logpoints.items(): - _set_log_level(logging.getLogger(key), value) + domain_config = hass.data[DOMAIN] = LoggerDomainConfig({}, settings) + logging.setLoggerClass(_get_logger_class(domain_config.overrides)) - # Set default log severity + websocket_api.async_load_websocket_api(hass) + + await settings.async_load() + + # Set default log severity and filter logger_config = config.get(DOMAIN, {}) if LOGGER_DEFAULT in logger_config: - set_default_log_level(logger_config[LOGGER_DEFAULT]) - - if LOGGER_LOGS in logger_config: - set_log_levels(config[DOMAIN][LOGGER_LOGS]) + set_default_log_level(hass, logger_config[LOGGER_DEFAULT]) if LOGGER_FILTERS in logger_config: - for key, value in logger_config[LOGGER_FILTERS].items(): - logger = logging.getLogger(key) - _add_log_filter(logger, value) + log_filters: dict[str, list[re.Pattern]] = logger_config[LOGGER_FILTERS] + for key, value in log_filters.items(): + _add_log_filter(logging.getLogger(key), value) + + # Combine log levels configured in configuration.yaml with log levels set by frontend + combined_logs = await settings.async_get_levels(hass) + set_log_levels(hass, combined_logs) @callback def async_service_handler(service: ServiceCall) -> None: """Handle logger services.""" if service.service == SERVICE_SET_DEFAULT_LEVEL: - set_default_log_level(service.data.get(ATTR_LEVEL)) + set_default_log_level(hass, service.data[ATTR_LEVEL]) else: - set_log_levels(service.data) + set_log_levels(hass, service.data) hass.services.async_register( DOMAIN, @@ -104,24 +102,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _set_log_level(logger, level): - """Set the log level. - - Any logger fetched before this integration is loaded will use old class. - """ - getattr(logger, "orig_setLevel", logger.setLevel)(LOGSEVERITY[level]) - - -def _add_log_filter(logger, patterns): +def _add_log_filter(logger: logging.Logger, patterns: list[re.Pattern]) -> None: """Add a Filter to the logger based on a regexp of the filter_str.""" - def filter_func(logrecord): + def filter_func(logrecord: logging.LogRecord) -> bool: return not any(p.search(logrecord.getMessage()) for p in patterns) logger.addFilter(filter_func) -def _get_logger_class(hass_overrides): +def _get_logger_class(hass_overrides: dict[str, int]) -> type[logging.Logger]: """Create a logger subclass. logging.setLoggerClass checks if it is a subclass of Logger and @@ -131,7 +121,7 @@ def _get_logger_class(hass_overrides): class HassLogger(logging.Logger): """Home Assistant aware logger class.""" - def setLevel(self, level) -> None: + def setLevel(self, level: int | str) -> None: """Set the log level unless overridden.""" if self.name in hass_overrides: return @@ -139,7 +129,7 @@ def _get_logger_class(hass_overrides): super().setLevel(level) # pylint: disable=invalid-name - def orig_setLevel(self, level) -> None: + def orig_setLevel(self, level: int | str) -> None: """Set the log level.""" super().setLevel(level) diff --git a/homeassistant/components/logger/const.py b/homeassistant/components/logger/const.py new file mode 100644 index 00000000000..06f2af4f3f5 --- /dev/null +++ b/homeassistant/components/logger/const.py @@ -0,0 +1,42 @@ +"""Constants for the Logger integration.""" +import logging + +DOMAIN = "logger" + +SERVICE_SET_DEFAULT_LEVEL = "set_default_level" +SERVICE_SET_LEVEL = "set_level" + +LOGSEVERITY_NOTSET = "NOTSET" +LOGSEVERITY_DEBUG = "DEBUG" +LOGSEVERITY_INFO = "INFO" +LOGSEVERITY_WARNING = "WARNING" +LOGSEVERITY_ERROR = "ERROR" +LOGSEVERITY_CRITICAL = "CRITICAL" +LOGSEVERITY_WARN = "WARN" +LOGSEVERITY_FATAL = "FATAL" + +LOGSEVERITY = { + LOGSEVERITY_CRITICAL: logging.CRITICAL, + LOGSEVERITY_FATAL: logging.FATAL, + LOGSEVERITY_ERROR: logging.ERROR, + LOGSEVERITY_WARNING: logging.WARNING, + LOGSEVERITY_WARN: logging.WARN, + LOGSEVERITY_INFO: logging.INFO, + LOGSEVERITY_DEBUG: logging.DEBUG, + LOGSEVERITY_NOTSET: logging.NOTSET, +} + + +DEFAULT_LOGSEVERITY = "DEBUG" + +LOGGER_DEFAULT = "default" +LOGGER_LOGS = "logs" +LOGGER_FILTERS = "filters" + +ATTR_LEVEL = "level" + +EVENT_LOGGING_CHANGED = "logging_changed" + +STORAGE_KEY = "core.logger" +STORAGE_LOG_KEY = "logs" +STORAGE_VERSION = 1 diff --git a/homeassistant/components/logger/helpers.py b/homeassistant/components/logger/helpers.py new file mode 100644 index 00000000000..d85486a41e0 --- /dev/null +++ b/homeassistant/components/logger/helpers.py @@ -0,0 +1,217 @@ +"""Helpers for the logger integration.""" +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Mapping +import contextlib +from dataclasses import asdict, dataclass +import logging +from typing import Any, cast + +from homeassistant.backports.enum import StrEnum +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import IntegrationNotFound, async_get_integration + +from .const import ( + DOMAIN, + EVENT_LOGGING_CHANGED, + LOGGER_DEFAULT, + LOGGER_LOGS, + LOGSEVERITY, + LOGSEVERITY_NOTSET, + STORAGE_KEY, + STORAGE_LOG_KEY, + STORAGE_VERSION, +) + + +@callback +def async_get_domain_config(hass: HomeAssistant) -> LoggerDomainConfig: + """Return the domain config.""" + return cast(LoggerDomainConfig, hass.data[DOMAIN]) + + +@callback +def set_default_log_level(hass: HomeAssistant, level: int) -> None: + """Set the default log level for components.""" + _set_log_level(logging.getLogger(""), level) + hass.bus.async_fire(EVENT_LOGGING_CHANGED) + + +@callback +def set_log_levels(hass: HomeAssistant, logpoints: Mapping[str, int]) -> None: + """Set the specified log levels.""" + async_get_domain_config(hass).overrides.update(logpoints) + for key, value in logpoints.items(): + _set_log_level(logging.getLogger(key), value) + hass.bus.async_fire(EVENT_LOGGING_CHANGED) + + +def _set_log_level(logger: logging.Logger, level: int) -> None: + """Set the log level. + + Any logger fetched before this integration is loaded will use old class. + """ + getattr(logger, "orig_setLevel", logger.setLevel)(level) + + +def _chattiest_log_level(level1: int, level2: int) -> int: + """Return the chattiest log level.""" + if level1 == logging.NOTSET: + return level2 + if level2 == logging.NOTSET: + return level1 + return min(level1, level2) + + +async def get_integration_loggers(hass: HomeAssistant, domain: str) -> list[str]: + """Get loggers for an integration.""" + loggers = [f"homeassistant.components.{domain}"] + with contextlib.suppress(IntegrationNotFound): + integration = await async_get_integration(hass, domain) + if integration.loggers: + loggers.extend(integration.loggers) + return loggers + + +@dataclass +class LoggerSetting: + """Settings for a single module or integration.""" + + level: str + persistence: str + type: str + + +@dataclass +class LoggerDomainConfig: + """Logger domain config.""" + + overrides: dict[str, Any] + settings: LoggerSettings + + +class LogPersistance(StrEnum): + """Log persistence.""" + + NONE = "none" + ONCE = "once" + PERMANENT = "permanent" + + +class LogSettingsType(StrEnum): + """Log settings type.""" + + INTEGRATION = "integration" + MODULE = "module" + + +class LoggerSettings: + """Manage log settings.""" + + _stored_config: dict[str, dict[str, LoggerSetting]] + + def __init__(self, hass: HomeAssistant, yaml_config: ConfigType) -> None: + """Initialize log settings.""" + + self._yaml_config = yaml_config + self._default_level = logging.INFO + if DOMAIN in yaml_config: + self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT] + self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store( + hass, STORAGE_VERSION, STORAGE_KEY + ) + + async def async_load(self) -> None: + """Load stored settings.""" + stored_config = await self._store.async_load() + if not stored_config: + self._stored_config = {STORAGE_LOG_KEY: {}} + return + + def reset_persistence(settings: LoggerSetting) -> LoggerSetting: + """Reset persistence.""" + if settings.persistence == LogPersistance.ONCE: + settings.persistence = LogPersistance.NONE + return settings + + stored_log_config = stored_config[STORAGE_LOG_KEY] + # Reset domains for which the overrides should only be applied once + self._stored_config = { + STORAGE_LOG_KEY: { + domain: reset_persistence(LoggerSetting(**settings)) + for domain, settings in stored_log_config.items() + } + } + await self._store.async_save(self._async_data_to_save()) + + @callback + def _async_data_to_save(self) -> dict[str, dict[str, dict[str, str]]]: + """Generate data to be saved.""" + stored_log_config = self._stored_config[STORAGE_LOG_KEY] + return { + STORAGE_LOG_KEY: { + domain: asdict(settings) + for domain, settings in stored_log_config.items() + if settings.persistence + in (LogPersistance.ONCE, LogPersistance.PERMANENT) + } + } + + @callback + def async_save(self) -> None: + """Save settings.""" + self._store.async_delay_save(self._async_data_to_save, 15) + + @callback + def _async_get_logger_logs(self) -> dict[str, int]: + """Get the logger logs.""" + logger_logs: dict[str, int] = self._yaml_config.get(DOMAIN, {}).get( + LOGGER_LOGS, {} + ) + return logger_logs + + async def async_update( + self, hass: HomeAssistant, domain: str, settings: LoggerSetting + ) -> None: + """Update settings.""" + stored_log_config = self._stored_config[STORAGE_LOG_KEY] + if settings.level == LOGSEVERITY_NOTSET: + stored_log_config.pop(domain, None) + else: + stored_log_config[domain] = settings + + self.async_save() + + if settings.type == LogSettingsType.INTEGRATION: + loggers = await get_integration_loggers(hass, domain) + else: + loggers = [domain] + + combined_logs = {logger: LOGSEVERITY[settings.level] for logger in loggers} + # Don't override the log levels with the ones from YAML + # since we want whatever the user is asking for to be honored. + + set_log_levels(hass, combined_logs) + + async def async_get_levels(self, hass: HomeAssistant) -> dict[str, int]: + """Get combination of levels from yaml and storage.""" + combined_logs = defaultdict(lambda: logging.CRITICAL) + for domain, settings in self._stored_config[STORAGE_LOG_KEY].items(): + if settings.type == LogSettingsType.INTEGRATION: + loggers = await get_integration_loggers(hass, domain) + else: + loggers = [domain] + + for logger in loggers: + combined_logs[logger] = LOGSEVERITY[settings.level] + + if yaml_log_settings := self._async_get_logger_logs(): + for domain, level in yaml_log_settings.items(): + combined_logs[domain] = _chattiest_log_level( + combined_logs[domain], level + ) + + return dict(combined_logs) diff --git a/homeassistant/components/logger/websocket_api.py b/homeassistant/components/logger/websocket_api.py new file mode 100644 index 00000000000..1b4e5cb36a6 --- /dev/null +++ b/homeassistant/components/logger/websocket_api.py @@ -0,0 +1,104 @@ +"""Websocket API handlers for the logger integration.""" +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.setup import async_get_loaded_integrations + +from .const import LOGSEVERITY +from .helpers import ( + LoggerSetting, + LogPersistance, + LogSettingsType, + async_get_domain_config, +) + + +@callback +def async_load_websocket_api(hass: HomeAssistant) -> None: + """Set up the websocket API.""" + websocket_api.async_register_command(hass, handle_integration_log_info) + websocket_api.async_register_command(hass, handle_integration_log_level) + websocket_api.async_register_command(hass, handle_module_log_level) + + +@websocket_api.websocket_command({vol.Required("type"): "logger/log_info"}) +@websocket_api.async_response +async def handle_integration_log_info( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle integrations logger info.""" + connection.send_result( + msg["id"], + [ + { + "domain": integration, + "level": logging.getLogger( + f"homeassistant.components.{integration}" + ).getEffectiveLevel(), + } + for integration in async_get_loaded_integrations(hass) + ], + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "logger/integration_log_level", + vol.Required("integration"): str, + vol.Required("level"): vol.In(LOGSEVERITY), + vol.Required("persistence"): vol.Coerce(LogPersistance), + } +) +@websocket_api.async_response +async def handle_integration_log_level( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle setting integration log level.""" + try: + await async_get_integration(hass, msg["integration"]) + except IntegrationNotFound: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Integration not found" + ) + return + await async_get_domain_config(hass).settings.async_update( + hass, + msg["integration"], + LoggerSetting( + level=msg["level"], + persistence=msg["persistence"], + type=LogSettingsType.INTEGRATION, + ), + ) + connection.send_message(websocket_api.messages.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "logger/log_level", + vol.Required("module"): str, + vol.Required("level"): vol.In(LOGSEVERITY), + vol.Required("persistence"): vol.Coerce(LogPersistance), + } +) +@websocket_api.async_response +async def handle_module_log_level( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle setting integration log level.""" + await async_get_domain_config(hass).settings.async_update( + hass, + msg["module"], + LoggerSetting( + level=msg["level"], + persistence=msg["persistence"], + type=LogSettingsType.MODULE, + ), + ) + connection.send_message(websocket_api.messages.result_message(msg["id"])) diff --git a/homeassistant/components/logi_circle/translations/de.json b/homeassistant/components/logi_circle/translations/de.json index bed8328c92e..fba8106c937 100644 --- a/homeassistant/components/logi_circle/translations/de.json +++ b/homeassistant/components/logi_circle/translations/de.json @@ -13,7 +13,7 @@ }, "step": { "auth": { - "description": "Folge dem Link unten und dr\u00fccke **Akzeptieren** um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden** . \n\n [Link] ({authorization_url})", + "description": "Folge dem Link unten und dr\u00fccke **Akzeptieren** um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden**. \n\n [Link] ({authorization_url})", "title": "Authentifizierung mit Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/sk.json b/homeassistant/components/logi_circle/translations/sk.json index 5ada995aa6e..da1f4892e03 100644 --- a/homeassistant/components/logi_circle/translations/sk.json +++ b/homeassistant/components/logi_circle/translations/sk.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie." + }, "error": { + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", "invalid_auth": "Neplatn\u00e9 overenie" } } diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index 5b3ecefa4ff..aa3ba0c3614 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -93,7 +93,7 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): _attr_current_humidity: float | None = None # type: ignore[assignment] _attr_temperature_unit = TEMP_CELSIUS - _attr_supported_features: int = ( + _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE diff --git a/homeassistant/components/lookin/media_player.py b/homeassistant/components/lookin/media_player.py index f0e9c7e5928..9e925836e11 100644 --- a/homeassistant/components/lookin/media_player.py +++ b/homeassistant/components/lookin/media_player.py @@ -82,7 +82,6 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity): ) -> None: """Init the lookin media player.""" self._attr_device_class = device_class - self._attr_supported_features: int = 0 super().__init__(coordinator, uuid, device, lookin_data) for function_name, feature in _FUNCTION_NAME_TO_FEATURE.items(): if function_name in self._function_names: diff --git a/homeassistant/components/lookin/translations/he.json b/homeassistant/components/lookin/translations/he.json index 3110857a512..e44e85f614d 100644 --- a/homeassistant/components/lookin/translations/he.json +++ b/homeassistant/components/lookin/translations/he.json @@ -4,11 +4,11 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/lookin/translations/sk.json b/homeassistant/components/lookin/translations/sk.json index 561644de2dd..ff1b0e0b78b 100644 --- a/homeassistant/components/lookin/translations/sk.json +++ b/homeassistant/components/lookin/translations/sk.json @@ -1,13 +1,30 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({host})", "step": { "device_name": { "data": { "name": "N\u00e1zov" } + }, + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP adresa" + } } } } diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index aed8d80f8b1..4c84c81af5e 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,7 +3,7 @@ "name": "Sensor.Community", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": ["luftdaten==0.7.2"], + "requirements": ["luftdaten==0.7.4"], "codeowners": ["@fabaff", "@frenck"], "quality_scale": "gold", "iot_class": "cloud_polling", diff --git a/homeassistant/components/luftdaten/translations/sk.json b/homeassistant/components/luftdaten/translations/sk.json new file mode 100644 index 00000000000..b1f2a309ba0 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_sensor": "Sn\u00edma\u010d nie je dostupn\u00fd alebo je neplatn\u00fd" + }, + "step": { + "user": { + "data": { + "show_on_map": "Zobrazi\u0165 na mape", + "station_id": "ID sn\u00edma\u010da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index cb526b004de..5792f186798 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -2,7 +2,7 @@ "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.1.9"], + "requirements": ["lupupy==0.2.1"], "codeowners": ["@majuss"], "iot_class": "local_polling", "loggers": ["lupupy"] diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 7c3e66c7127..cc002539d6b 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": ["pylutron==0.2.8"], - "codeowners": ["@JonGilmore"], + "codeowners": ["@cdheiser"], "iot_class": "local_polling", "loggers": ["pylutron"] } diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 385fdf94a62..5ee64a687bf 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -6,7 +6,7 @@ import contextlib from itertools import chain import logging import ssl -from typing import Any +from typing import Any, cast import async_timeout from pylutron_caseta import BUTTON_STATUS_PRESSED @@ -28,6 +28,7 @@ from .const import ( ATTR_ACTION, ATTR_AREA_NAME, ATTR_BUTTON_NUMBER, + ATTR_BUTTON_TYPE, ATTR_DEVICE_NAME, ATTR_LEAP_BUTTON_NUMBER, ATTR_SERIAL, @@ -50,7 +51,21 @@ from .device_trigger import ( LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP, LUTRON_BUTTON_TRIGGER_SCHEMA, ) -from .models import LutronButton, LutronCasetaData, LutronKeypad, LutronKeypadData +from .models import ( + LUTRON_BUTTON_LEAP_BUTTON_NUMBER, + LUTRON_KEYPAD_AREA_NAME, + LUTRON_KEYPAD_BUTTONS, + LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID, + LUTRON_KEYPAD_LUTRON_DEVICE_ID, + LUTRON_KEYPAD_MODEL, + LUTRON_KEYPAD_NAME, + LUTRON_KEYPAD_SERIAL, + LUTRON_KEYPAD_TYPE, + LutronButton, + LutronCasetaData, + LutronKeypad, + LutronKeypadData, +) from .util import serial_to_unique_id _LOGGER = logging.getLogger(__name__) @@ -225,57 +240,77 @@ def _async_setup_keypads( hass: HomeAssistant, config_entry_id: str, bridge: Smartbridge, - bridge_device: dict[str, Any], + bridge_device: dict[str, str | int], ) -> LutronKeypadData: """Register keypad devices (Keypads and Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) - bridge_devices = bridge.get_devices() - bridge_buttons = bridge.buttons + bridge_devices: dict[str, dict[str, str | int]] = bridge.get_devices() + bridge_buttons: dict[str, dict[str, str | int]] = bridge.buttons dr_device_id_to_keypad: dict[str, LutronKeypad] = {} keypads: dict[int, LutronKeypad] = {} keypad_buttons: dict[int, LutronButton] = {} keypad_button_names_to_leap: dict[int, dict[str, int]] = {} + leap_to_keypad_button_names: dict[int, dict[int, str]] = {} for bridge_button in bridge_buttons.values(): - bridge_keypad = bridge_devices[bridge_button["parent_device"]] - keypad_device_id = bridge_keypad["device_id"] - button_device_id = bridge_button["device_id"] + parent_device = cast(str, bridge_button["parent_device"]) + bridge_keypad = bridge_devices[parent_device] + keypad_lutron_device_id = cast(int, bridge_keypad["device_id"]) + button_lutron_device_id = cast(int, bridge_button["device_id"]) + leap_button_number = cast(int, bridge_button["button_number"]) + button_led_device_id = None + if "button_led" in bridge_button: + button_led_device_id = cast(str, bridge_button["button_led"]) - if not (keypad := keypads.get(keypad_device_id)): + if not (keypad := keypads.get(keypad_lutron_device_id)): # First time seeing this keypad, build keypad data and store in keypads - keypad = keypads[keypad_device_id] = _async_build_lutron_keypad( - bridge, bridge_device, bridge_keypad, keypad_device_id + keypad = keypads[keypad_lutron_device_id] = _async_build_lutron_keypad( + bridge, bridge_device, bridge_keypad, keypad_lutron_device_id ) # Register the keypad device dr_device = device_registry.async_get_or_create( **keypad["device_info"], config_entry_id=config_entry_id ) - keypad["dr_device_id"] = dr_device.id + keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID] = dr_device.id dr_device_id_to_keypad[dr_device.id] = keypad + button_name = _get_button_name(keypad, bridge_button) + keypad_lutron_device_id = keypad[LUTRON_KEYPAD_LUTRON_DEVICE_ID] + # Add button to parent keypad, and build keypad_buttons and keypad_button_names_to_leap - button = keypad_buttons[button_device_id] = LutronButton( - lutron_device_id=button_device_id, - leap_button_number=bridge_button["button_number"], - button_name=_get_button_name(keypad, bridge_button), - led_device_id=bridge_button.get("button_led"), - parent_keypad=keypad["lutron_device_id"], + keypad_buttons[button_lutron_device_id] = LutronButton( + lutron_device_id=button_lutron_device_id, + leap_button_number=leap_button_number, + button_name=button_name, + led_device_id=button_led_device_id, + parent_keypad=keypad_lutron_device_id, ) - keypad["buttons"].append(button["lutron_device_id"]) + keypad[LUTRON_KEYPAD_BUTTONS].append(button_lutron_device_id) - keypad_button_names_to_leap.setdefault(keypad["lutron_device_id"], {}).update( - {button["button_name"]: int(button["leap_button_number"])} + button_name_to_leap = keypad_button_names_to_leap.setdefault( + keypad_lutron_device_id, {} ) + button_name_to_leap[button_name] = leap_button_number + leap_to_button_name = leap_to_keypad_button_names.setdefault( + keypad_lutron_device_id, {} + ) + leap_to_button_name[leap_button_number] = button_name keypad_trigger_schemas = _async_build_trigger_schemas(keypad_button_names_to_leap) - _async_subscribe_keypad_events(hass, bridge, keypads, keypad_buttons) + _async_subscribe_keypad_events( + hass=hass, + bridge=bridge, + keypads=keypads, + keypad_buttons=keypad_buttons, + leap_to_keypad_button_names=leap_to_keypad_button_names, + ) return LutronKeypadData( dr_device_id_to_keypad, @@ -312,7 +347,6 @@ def _async_build_lutron_keypad( keypad_device_id: int, ) -> LutronKeypad: # First time seeing this keypad, build keypad data and store in keypads - area_name = _area_name_from_id(bridge.areas, bridge_keypad["area"]) keypad_name = bridge_keypad["name"].split("_")[-1] keypad_serial = _handle_none_keypad_serial(bridge_keypad, bridge_device["serial"]) @@ -350,7 +384,7 @@ def _get_button_name(keypad: LutronKeypad, bridge_button: dict[str, Any]) -> str # This is a Caseta Button retrieve name from hardcoded trigger definitions. return _get_button_name_from_triggers(keypad, button_number) - keypad_model = keypad["model"] + keypad_model = keypad[LUTRON_KEYPAD_MODEL] if keypad_model_override := KEYPAD_LEAP_BUTTON_NAME_OVERRIDE.get(keypad_model): if alt_button_name := keypad_model_override.get(button_number): return alt_button_name @@ -412,8 +446,9 @@ def async_get_lip_button(device_type: str, leap_button: int) -> int | None: def _async_subscribe_keypad_events( hass: HomeAssistant, bridge: Smartbridge, - keypads: dict[int, Any], - keypad_buttons: dict[int, Any], + keypads: dict[int, LutronKeypad], + keypad_buttons: dict[int, LutronButton], + leap_to_keypad_button_names: dict[int, dict[int, str]], ): """Subscribe to lutron events.""" @@ -429,20 +464,25 @@ def _async_subscribe_keypad_events( else: action = ACTION_RELEASE - keypad_type = keypad["type"] - leap_button_number = button["leap_button_number"] + keypad_type = keypad[LUTRON_KEYPAD_TYPE] + keypad_device_id = keypad[LUTRON_KEYPAD_LUTRON_DEVICE_ID] + leap_button_number = button[LUTRON_BUTTON_LEAP_BUTTON_NUMBER] lip_button_number = async_get_lip_button(keypad_type, leap_button_number) + button_type = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get( + keypad_type, leap_to_keypad_button_names[keypad_device_id] + )[leap_button_number] hass.bus.async_fire( LUTRON_CASETA_BUTTON_EVENT, { - ATTR_SERIAL: keypad["serial"], + ATTR_SERIAL: keypad[LUTRON_KEYPAD_SERIAL], ATTR_TYPE: keypad_type, ATTR_BUTTON_NUMBER: lip_button_number, ATTR_LEAP_BUTTON_NUMBER: leap_button_number, - ATTR_DEVICE_NAME: keypad["name"], - ATTR_DEVICE_ID: keypad["dr_device_id"], - ATTR_AREA_NAME: keypad["area_name"], + ATTR_DEVICE_NAME: keypad[LUTRON_KEYPAD_NAME], + ATTR_DEVICE_ID: keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID], + ATTR_AREA_NAME: keypad[LUTRON_KEYPAD_AREA_NAME], + ATTR_BUTTON_TYPE: button_type, ATTR_ACTION: action, }, ) diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index ae8dc0a505a..af06bf0e0f0 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -18,6 +18,7 @@ MANUFACTURER = "Lutron Electronics Co., Inc" ATTR_SERIAL = "serial" ATTR_TYPE = "type" +ATTR_BUTTON_TYPE = "button_type" ATTR_LEAP_BUTTON_NUMBER = "leap_button_number" ATTR_BUTTON_NUMBER = "button_number" # LIP button number ATTR_DEVICE_NAME = "device_name" diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 2dfbe526c93..7e178698afe 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -6,9 +6,6 @@ import logging import voluptuous as vol from homeassistant.components.device_automation import DEVICE_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, @@ -18,7 +15,6 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -26,8 +22,7 @@ from .const import ( ACTION_PRESS, ACTION_RELEASE, ATTR_ACTION, - ATTR_LEAP_BUTTON_NUMBER, - ATTR_SERIAL, + ATTR_BUTTON_TYPE, CONF_SUBTYPE, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, @@ -317,7 +312,7 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = { "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP, } -LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP = { +LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP: dict[str, dict[int, str]] = { k: _reverse_dict(v) for k, v in DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.items() } @@ -421,53 +416,22 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - device_id = config[CONF_DEVICE_ID] - subtype = config[CONF_SUBTYPE] - if not (data := get_lutron_data_by_dr_id(hass, device_id)) or not ( - keypad := data.keypad_data.dr_device_id_to_keypad[device_id] - ): - raise HomeAssistantError( - f"Cannot attach trigger {config} because device with id {device_id} is missing or invalid" - ) - - keypad_trigger_schemas = data.keypad_data.trigger_schemas - keypad_button_names_to_leap = data.keypad_data.button_names_to_leap - - device_type = keypad["type"] - serial = keypad["serial"] - lutron_device_id = keypad["lutron_device_id"] - - # Retrieve trigger schema, preferring hard-coded triggers from device_trigger.py - schema = DEVICE_TYPE_SCHEMA_MAP.get( - device_type, - keypad_trigger_schemas[lutron_device_id], - ) - - # Retrieve list of valid buttons, preferring hard-coded triggers from device_trigger.py - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get( - device_type, - keypad_button_names_to_leap[lutron_device_id], - ) - - if subtype not in valid_buttons: - raise InvalidDeviceAutomationConfig( - f"Cannot attach trigger {config} because subtype {subtype} is invalid" - ) - - config = schema(config) - event_config = { - event_trigger.CONF_PLATFORM: CONF_EVENT, - event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, - event_trigger.CONF_EVENT_DATA: { - ATTR_SERIAL: serial, - ATTR_LEAP_BUTTON_NUMBER: valid_buttons[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, trigger_info, platform_type="device" + hass, + event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, + event_trigger.CONF_EVENT_DATA: { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + ATTR_ACTION: config[CONF_TYPE], + ATTR_BUTTON_TYPE: config[CONF_SUBTYPE], + }, + } + ), + action, + trigger_info, + platform_type="device", ) diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 576387bd36b..61f00a1b09f 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, TypedDict +from typing import Any, Final, TypedDict from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol @@ -45,11 +45,30 @@ class LutronKeypad(TypedDict): buttons: list[int] +LUTRON_KEYPAD_LUTRON_DEVICE_ID: Final = "lutron_device_id" +LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID: Final = "dr_device_id" +LUTRON_KEYPAD_AREA_ID: Final = "area_id" +LUTRON_KEYPAD_AREA_NAME: Final = "area_name" +LUTRON_KEYPAD_NAME: Final = "name" +LUTRON_KEYPAD_SERIAL: Final = "serial" +LUTRON_KEYPAD_DEVICE_INFO: Final = "device_info" +LUTRON_KEYPAD_MODEL: Final = "model" +LUTRON_KEYPAD_TYPE: Final = "type" +LUTRON_KEYPAD_BUTTONS: Final = "buttons" + + class LutronButton(TypedDict): """A lutron_caseta button.""" lutron_device_id: int leap_button_number: int button_name: str - led_device_id: int + led_device_id: str | None parent_keypad: int + + +LUTRON_BUTTON_LUTRON_DEVICE_ID: Final = "lutron_device_id" +LUTRON_BUTTON_LEAP_BUTTON_NUMBER: Final = "leap_button_number" +LUTRON_BUTTON_BUTTON_NAME: Final = "button_name" +LUTRON_BUTTON_LED_DEVICE_ID: Final = "led_device_id" +LUTRON_BUTTON_PARENT_KEYPAD: Final = "parent_keypad" diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index ff4f89aa512..05256098c62 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -58,17 +58,17 @@ "open_3": "\u00d6ffnen 3", "open_4": "\u00d6ffnen 4", "open_all": "Alle \u00f6ffnen", - "raise": "Raise", + "raise": "Erh\u00f6hen", "raise_1": "Anheben 1", "raise_2": "Anheben 2", "raise_3": "Anheben 3", "raise_4": "Anheben 4", "raise_all": "Erhebe alle", - "stop": "Stop (Favorit)", - "stop_1": "Stop 1", - "stop_2": "Stop 2", - "stop_3": "Stop 3", - "stop_4": "Stop 4", + "stop": "Stopp (Favorit)", + "stop_1": "Stopp 1", + "stop_2": "Stopp 2", + "stop_3": "Stopp 3", + "stop_4": "Stopp 4", "stop_all": "Alle anhalten" }, "trigger_type": { diff --git a/homeassistant/components/lutron_caseta/translations/el.json b/homeassistant/components/lutron_caseta/translations/el.json index f0c1ec35450..87e52ca850f 100644 --- a/homeassistant/components/lutron_caseta/translations/el.json +++ b/homeassistant/components/lutron_caseta/translations/el.json @@ -33,6 +33,9 @@ "button_2": "\u0394\u03b5\u03cd\u03c4\u03b5\u03c1\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "button_3": "\u03a4\u03c1\u03af\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "button_4": "\u03a4\u03ad\u03c4\u03b1\u03c1\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", + "button_5": "\u03a0\u03ad\u03bc\u03c0\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", + "button_6": "\u0388\u03ba\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", + "button_7": "\u0388\u03b2\u03b4\u03bf\u03bc\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "close_1": "\u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf 1", "close_2": "\u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf 2", "close_3": "\u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf 3", diff --git a/homeassistant/components/lutron_caseta/translations/he.json b/homeassistant/components/lutron_caseta/translations/he.json index cb742b61b72..5bebea072fc 100644 --- a/homeassistant/components/lutron_caseta/translations/he.json +++ b/homeassistant/components/lutron_caseta/translations/he.json @@ -19,7 +19,7 @@ "data": { "host": "\u05de\u05d0\u05e8\u05d7" }, - "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4- IP \u05e9\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8." + "description": "\u05d9\u05e9 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4-IP \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df." } } } diff --git a/homeassistant/components/lutron_caseta/translations/sk.json b/homeassistant/components/lutron_caseta/translations/sk.json new file mode 100644 index 00000000000..11557dadc82 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/sk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "not_lutron_device": "Zisten\u00e9 zariadenie nie je zariadenie Lutron" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Zadajte IP adresu zariadenia.", + "title": "Automaticky sa pripojte k mostu" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Prv\u00e9 tla\u010didlo", + "button_2": "Druh\u00e9 tla\u010didlo", + "button_3": "Tretie tla\u010didlo", + "button_4": "\u0160tvrt\u00e9 tla\u010didlo", + "button_5": "Piate tla\u010didlo", + "button_6": "\u0160ieste tla\u010didlo", + "button_7": "Siedme tla\u010didlo", + "group_1_button_2": "Prv\u00e1 skupina druh\u00e9 tla\u010didlo", + "group_2_button_1": "Druh\u00e1 skupina prv\u00e9 tla\u010didlo", + "group_2_button_2": "Druh\u00e1 skupina druh\u00e9 tla\u010didlo" + }, + "trigger_type": { + "press": "\"{subtype}\" stla\u010den\u00e9", + "release": "\u201c{subtype}\u201c uvo\u013enen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index ae4afa0b0c6..8339c4dad45 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -169,13 +169,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): self.entity_description = description @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" if self.device.changeableValues.thermostatSetpointStatus: - support_flags = SUPPORT_FLAGS_LCC - else: - support_flags = SUPPORT_FLAGS_TCC - return support_flags + return SUPPORT_FLAGS_LCC + return SUPPORT_FLAGS_TCC @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index c0d9168f46f..6101101bf70 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", "dependencies": ["application_credentials"], - "requirements": ["aiolyric==1.0.8"], + "requirements": ["aiolyric==1.0.9"], "codeowners": ["@timmo001"], "quality_scale": "silver", "dhcp": [ diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 4d132381d42..528161f3d6e 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -90,6 +90,22 @@ async def async_setup_entry( device, ) ) + if device.indoorHumidity: + entities.append( + LyricSensor( + coordinator, + LyricSensorEntityDescription( + key=f"{device.macID}_indoor_humidity", + name="Indoor Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value=lambda device: device.indoorHumidity, + ), + location, + device, + ) + ) if device.outdoorTemperature: entities.append( LyricSensor( diff --git a/homeassistant/components/lyric/translations/bg.json b/homeassistant/components/lyric/translations/bg.json index 5d9459cac2c..0cca3507e7f 100644 --- a/homeassistant/components/lyric/translations/bg.json +++ b/homeassistant/components/lyric/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/lyric/translations/sk.json b/homeassistant/components/lyric/translations/sk.json index 520a3afd6d9..6a514a50f33 100644 --- a/homeassistant/components/lyric/translations/sk.json +++ b/homeassistant/components/lyric/translations/sk.json @@ -1,10 +1,20 @@ { "config": { "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/hr.json b/homeassistant/components/mailgun/translations/hr.json new file mode 100644 index 00000000000..90563173adf --- /dev/null +++ b/homeassistant/components/mailgun/translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Jeste li sigurni da \u017eelite postaviti Mailgun?", + "title": "Postavite Mailgun Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/sk.json b/homeassistant/components/mailgun/translations/sk.json new file mode 100644 index 00000000000..27eb9ec5b61 --- /dev/null +++ b/homeassistant/components/mailgun/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + }, + "step": { + "user": { + "description": "Naozaj chcete nastavi\u0165 Mailgun?", + "title": "Nastavte si Mailgun Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py new file mode 100644 index 00000000000..845d48ea883 --- /dev/null +++ b/homeassistant/components/matter/__init__.py @@ -0,0 +1,351 @@ +"""The Matter integration.""" +from __future__ import annotations + +import asyncio +from typing import cast + +import async_timeout +from matter_server.client import MatterClient +from matter_server.client.exceptions import ( + CannotConnect, + FailedCommand, + InvalidServerVersion, +) +import voluptuous as vol + +from homeassistant.components.hassio import AddonError, AddonManager, AddonState +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) +from homeassistant.helpers.service import async_register_admin_service + +from .adapter import MatterAdapter +from .addon import get_addon_manager +from .api import async_register_api +from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, LOGGER +from .device_platform import DEVICE_PLATFORM + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Matter from a config entry.""" + if use_addon := entry.data.get(CONF_USE_ADDON): + await _async_ensure_addon_running(hass, entry) + + matter_client = MatterClient(entry.data[CONF_URL], async_get_clientsession(hass)) + try: + await matter_client.connect() + except CannotConnect as err: + raise ConfigEntryNotReady("Failed to connect to matter server") from err + except InvalidServerVersion as err: + if use_addon: + addon_manager = _get_addon_manager(hass) + addon_manager.async_schedule_update_addon(catch_error=True) + else: + async_create_issue( + hass, + DOMAIN, + "invalid_server_version", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="invalid_server_version", + ) + raise ConfigEntryNotReady(f"Invalid server version: {err}") from err + + except Exception as err: + matter_client.logger.exception("Failed to connect to matter server") + raise ConfigEntryNotReady( + "Unknown error connecting to the Matter server" + ) from err + else: + async_delete_issue(hass, DOMAIN, "invalid_server_version") + + async def on_hass_stop(event: Event) -> None: + """Handle incoming stop event from Home Assistant.""" + await matter_client.disconnect() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) + + # register websocket api + async_register_api(hass) + + # launch the matter client listen task in the background + # use the init_ready event to keep track if it did initialize successfully + init_ready = asyncio.Event() + listen_task = asyncio.create_task(matter_client.start_listening(init_ready)) + + try: + async with async_timeout.timeout(30): + await init_ready.wait() + except asyncio.TimeoutError as err: + listen_task.cancel() + raise ConfigEntryNotReady("Matter client not ready") from err + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + _async_init_services(hass) + + # we create an intermediate layer (adapter) which keeps track of our nodes + # and discovery of platform entities from the node's attributes + matter = MatterAdapter(hass, matter_client, entry) + hass.data[DOMAIN][entry.entry_id] = matter + + # forward platform setup to all platforms in the discovery schema + await hass.config_entries.async_forward_entry_setups(entry, DEVICE_PLATFORM) + + # start discovering of node entities as task + asyncio.create_task(matter.setup_nodes()) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, DEVICE_PLATFORM) + + if unload_ok: + matter: MatterAdapter = hass.data[DOMAIN].pop(entry.entry_id) + await matter.matter_client.disconnect() + + if entry.data.get(CONF_USE_ADDON) and entry.disabled_by: + addon_manager: AddonManager = get_addon_manager(hass) + LOGGER.debug("Stopping Matter Server add-on") + try: + await addon_manager.async_stop_addon() + except AddonError as err: + LOGGER.error("Failed to stop the Matter Server add-on: %s", err) + return False + + return unload_ok + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Config entry is being removed.""" + + if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON): + return + + addon_manager: AddonManager = get_addon_manager(hass) + try: + await addon_manager.async_stop_addon() + except AddonError as err: + LOGGER.error(err) + return + try: + await addon_manager.async_create_backup() + except AddonError as err: + LOGGER.error(err) + return + try: + await addon_manager.async_uninstall_addon() + except AddonError as err: + LOGGER.error(err) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + unique_id = None + + for ident in device_entry.identifiers: + if ident[0] == DOMAIN: + unique_id = ident[1] + break + + if not unique_id: + return True + + matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id] + + for node in await matter.matter_client.get_nodes(): + if node.unique_id == unique_id: + await matter.matter_client.remove_node(node.node_id) + break + + return True + + +@callback +def get_matter(hass: HomeAssistant) -> MatterAdapter: + """Return MatterAdapter instance.""" + # NOTE: This assumes only one Matter connection/fabric can exist. + # Shall we support connecting to multiple servers in the client or by config entries? + # In case of the config entry we need to fix this. + matter: MatterAdapter = next(iter(hass.data[DOMAIN].values())) + return matter + + +@callback +def _async_init_services(hass: HomeAssistant) -> None: + """Init services.""" + + async def commission(call: ServiceCall) -> None: + """Handle commissioning.""" + matter_client = get_matter(hass).matter_client + try: + await matter_client.commission_with_code(call.data["code"]) + except FailedCommand as err: + raise HomeAssistantError(str(err)) from err + + async_register_admin_service( + hass, + DOMAIN, + "commission", + commission, + vol.Schema({"code": str}), + ) + + async def accept_shared_device(call: ServiceCall) -> None: + """Accept a shared device.""" + matter_client = get_matter(hass).matter_client + try: + await matter_client.commission_on_network(call.data["pin"]) + except FailedCommand as err: + raise HomeAssistantError(str(err)) from err + + async_register_admin_service( + hass, + DOMAIN, + "accept_shared_device", + accept_shared_device, + vol.Schema({"pin": vol.Coerce(int)}), + ) + + async def set_wifi(call: ServiceCall) -> None: + """Handle set wifi creds.""" + matter_client = get_matter(hass).matter_client + try: + await matter_client.set_wifi_credentials( + call.data["ssid"], call.data["password"] + ) + except FailedCommand as err: + raise HomeAssistantError(str(err)) from err + + async_register_admin_service( + hass, + DOMAIN, + "set_wifi", + set_wifi, + vol.Schema( + { + "ssid": str, + "password": str, + } + ), + ) + + async def set_thread_dataset(call: ServiceCall) -> None: + """Handle set Thread creds.""" + matter_client = get_matter(hass).matter_client + thread_dataset = bytes.fromhex(call.data["dataset"]) + try: + await matter_client.set_thread_operational_dataset(thread_dataset) + except FailedCommand as err: + raise HomeAssistantError(str(err)) from err + + async_register_admin_service( + hass, + DOMAIN, + "set_thread", + set_thread_dataset, + vol.Schema({"dataset": str}), + ) + + async def _node_id_from_ha_device_id(ha_device_id: str) -> int | None: + """Get node id from ha device id.""" + dev_reg = dr.async_get(hass) + device = dev_reg.async_get(ha_device_id) + + if device is None: + return None + + matter_id = next( + ( + identifier + for identifier in device.identifiers + if identifier[0] == DOMAIN + ), + None, + ) + + if not matter_id: + return None + + unique_id = matter_id[1] + + matter_client = get_matter(hass).matter_client + + # This could be more efficient + for node in await matter_client.get_nodes(): + if node.unique_id == unique_id: + return cast(int, node.node_id) + + return None + + async def open_commissioning_window(call: ServiceCall) -> None: + """Open commissioning window on specific node.""" + node_id = await _node_id_from_ha_device_id(call.data["device_id"]) + + if node_id is None: + raise HomeAssistantError("This is not a Matter device") + + matter_client = get_matter(hass).matter_client + + # We are sending device ID . + + try: + await matter_client.open_commissioning_window(node_id) + except FailedCommand as err: + raise HomeAssistantError(str(err)) from err + + async_register_admin_service( + hass, + DOMAIN, + "open_commissioning_window", + open_commissioning_window, + vol.Schema({"device_id": str}), + ) + + +async def _async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Ensure that Matter Server add-on is installed and running.""" + addon_manager = _get_addon_manager(hass) + try: + addon_info = await addon_manager.async_get_addon_info() + except AddonError as err: + raise ConfigEntryNotReady(err) from err + + addon_state = addon_info.state + + if addon_state == AddonState.NOT_INSTALLED: + addon_manager.async_schedule_install_setup_addon( + addon_info.options, + catch_error=True, + ) + raise ConfigEntryNotReady + + if addon_state == AddonState.NOT_RUNNING: + addon_manager.async_schedule_start_addon(catch_error=True) + raise ConfigEntryNotReady + + +@callback +def _get_addon_manager(hass: HomeAssistant) -> AddonManager: + """Ensure that Matter Server add-on is updated and running. + + May only be used as part of async_setup_entry above. + """ + addon_manager: AddonManager = get_addon_manager(hass) + if addon_manager.task_in_progress(): + raise ConfigEntryNotReady + return addon_manager diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py new file mode 100644 index 00000000000..c2ad11cb10f --- /dev/null +++ b/homeassistant/components/matter/adapter.py @@ -0,0 +1,141 @@ +"""Matter to Home Assistant adapter.""" +from __future__ import annotations + +import asyncio +import logging +from typing import TYPE_CHECKING + +from chip.clusters import Objects as all_clusters +from matter_server.common.models.node_device import AbstractMatterNodeDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device_platform import DEVICE_PLATFORM + +if TYPE_CHECKING: + from matter_server.client import MatterClient + from matter_server.common.models.node import MatterNode + + +class MatterAdapter: + """Connect Matter into Home Assistant.""" + + def __init__( + self, + hass: HomeAssistant, + matter_client: MatterClient, + config_entry: ConfigEntry, + ) -> None: + """Initialize the adapter.""" + self.matter_client = matter_client + self.hass = hass + self.config_entry = config_entry + self.logger = logging.getLogger(__name__) + self.platform_handlers: dict[Platform, AddEntitiesCallback] = {} + self._platforms_set_up = asyncio.Event() + + def register_platform_handler( + self, platform: Platform, add_entities: AddEntitiesCallback + ) -> None: + """Register a platform handler.""" + self.platform_handlers[platform] = add_entities + if len(self.platform_handlers) == len(DEVICE_PLATFORM): + self._platforms_set_up.set() + + async def setup_nodes(self) -> None: + """Set up all existing nodes.""" + await self._platforms_set_up.wait() + for node in await self.matter_client.get_nodes(): + await self._setup_node(node) + + async def _setup_node(self, node: MatterNode) -> None: + """Set up an node.""" + self.logger.debug("Setting up entities for node %s", node.node_id) + + bridge_unique_id: str | None = None + + if node.aggregator_device_type_instance is not None: + node_info = node.root_device_type_instance.get_cluster(all_clusters.Basic) + self._create_device_registry( + node_info, node_info.nodeLabel or "Hub device", None + ) + bridge_unique_id = node_info.uniqueID + + for node_device in node.node_devices: + self._setup_node_device(node_device, bridge_unique_id) + + def _create_device_registry( + self, + info: all_clusters.Basic | all_clusters.BridgedDeviceBasic, + name: str, + bridge_unique_id: str | None, + ) -> None: + """Create a device registry entry.""" + dr.async_get(self.hass).async_get_or_create( + name=name, + config_entry_id=self.config_entry.entry_id, + identifiers={(DOMAIN, info.uniqueID)}, + hw_version=info.hardwareVersionString, + sw_version=info.softwareVersionString, + manufacturer=info.vendorName, + model=info.productName, + via_device=(DOMAIN, bridge_unique_id) if bridge_unique_id else None, + ) + + def _setup_node_device( + self, node_device: AbstractMatterNodeDevice, bridge_unique_id: str | None + ) -> None: + """Set up a node device.""" + node = node_device.node() + basic_info = node_device.device_info() + device_type_instances = node_device.device_type_instances() + + name = basic_info.nodeLabel + if not name and device_type_instances: + name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node.node_id}" + + self._create_device_registry(basic_info, name, bridge_unique_id) + + for instance in device_type_instances: + created = False + + for platform, devices in DEVICE_PLATFORM.items(): + entity_descriptions = devices.get(instance.device_type) + + if entity_descriptions is None: + continue + + if not isinstance(entity_descriptions, list): + entity_descriptions = [entity_descriptions] + + entities = [] + for entity_description in entity_descriptions: + self.logger.debug( + "Creating %s entity for %s (%s)", + platform, + instance.device_type.__name__, + hex(instance.device_type.device_type), + ) + entities.append( + entity_description.entity_cls( + self.matter_client, + node_device, + instance, + entity_description, + ) + ) + + self.platform_handlers[platform](entities) + created = True + + if not created: + self.logger.warning( + "Found unsupported device %s (%s)", + type(instance).__name__, + hex(instance.device_type.device_type), + ) diff --git a/homeassistant/components/matter/addon.py b/homeassistant/components/matter/addon.py new file mode 100644 index 00000000000..84f430a58d8 --- /dev/null +++ b/homeassistant/components/matter/addon.py @@ -0,0 +1,17 @@ +"""Provide add-on management.""" +from __future__ import annotations + +from homeassistant.components.hassio import AddonManager +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.singleton import singleton + +from .const import ADDON_SLUG, DOMAIN, LOGGER + +DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager" + + +@singleton(DATA_ADDON_MANAGER) +@callback +def get_addon_manager(hass: HomeAssistant) -> AddonManager: + """Get the add-on manager.""" + return AddonManager(hass, LOGGER, "Matter Server", ADDON_SLUG) diff --git a/homeassistant/components/matter/api.py b/homeassistant/components/matter/api.py new file mode 100644 index 00000000000..36cf83fd0da --- /dev/null +++ b/homeassistant/components/matter/api.py @@ -0,0 +1,152 @@ +"""Handle websocket api for Matter.""" +from __future__ import annotations + +from collections.abc import Callable +from functools import wraps +from typing import Any + +from matter_server.client.exceptions import FailedCommand +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api import ActiveConnection +from homeassistant.core import HomeAssistant, callback + +from .adapter import MatterAdapter +from .const import DOMAIN + +ID = "id" +TYPE = "type" + + +@callback +def async_register_api(hass: HomeAssistant) -> None: + """Register all of our api endpoints.""" + websocket_api.async_register_command(hass, websocket_commission) + websocket_api.async_register_command(hass, websocket_commission_on_network) + websocket_api.async_register_command(hass, websocket_set_thread_dataset) + websocket_api.async_register_command(hass, websocket_set_wifi_credentials) + + +def async_get_matter_adapter(func: Callable) -> Callable: + """Decorate function to get the MatterAdapter.""" + + @wraps(func) + async def _get_matter( + hass: HomeAssistant, connection: ActiveConnection, msg: dict + ) -> None: + """Provide the Matter client to the function.""" + matter: MatterAdapter = next(iter(hass.data[DOMAIN].values())) + + await func(hass, connection, msg, matter) + + return _get_matter + + +def async_handle_failed_command(func: Callable) -> Callable: + """Decorate function to handle FailedCommand and send relevant error.""" + + @wraps(func) + async def async_handle_failed_command_func( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], + *args: Any, + **kwargs: Any, + ) -> None: + """Handle FailedCommand within function and send relevant error.""" + try: + await func(hass, connection, msg, *args, **kwargs) + except FailedCommand as err: + connection.send_error(msg[ID], err.error_code, err.args[0]) + + return async_handle_failed_command_func + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "matter/commission", + vol.Required("code"): str, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_matter_adapter +async def websocket_commission( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], + matter: MatterAdapter, +) -> None: + """Add a device to the network and commission the device.""" + await matter.matter_client.commission_with_code(msg["code"]) + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "matter/commission_on_network", + vol.Required("pin"): int, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_matter_adapter +async def websocket_commission_on_network( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], + matter: MatterAdapter, +) -> None: + """Commission a device already on the network.""" + await matter.matter_client.commission_on_network(msg["pin"]) + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "matter/set_thread", + vol.Required("thread_operation_dataset"): str, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_matter_adapter +async def websocket_set_thread_dataset( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], + matter: MatterAdapter, +) -> None: + """Set thread dataset.""" + await matter.matter_client.set_thread_operational_dataset( + msg["thread_operation_dataset"] + ) + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "matter/set_wifi_credentials", + vol.Required("network_name"): str, + vol.Required("password"): str, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_matter_adapter +async def websocket_set_wifi_credentials( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], + matter: MatterAdapter, +) -> None: + """Set WiFi credentials for a device.""" + await matter.matter_client.set_wifi_credentials( + ssid=msg["network_name"], credentials=msg["password"] + ) + connection.send_result(msg[ID]) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py new file mode 100644 index 00000000000..a4ca54920fb --- /dev/null +++ b/homeassistant/components/matter/binary_sensor.py @@ -0,0 +1,95 @@ +"""Matter binary sensors.""" +from __future__ import annotations + +from dataclasses import dataclass +from functools import partial +from typing import TYPE_CHECKING + +from chip.clusters import Objects as clusters +from matter_server.common.models import device_types + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import MatterEntity, MatterEntityDescriptionBaseClass + +if TYPE_CHECKING: + from .adapter import MatterAdapter + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter binary sensor from Config Entry.""" + matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id] + matter.register_platform_handler(Platform.BINARY_SENSOR, async_add_entities) + + +class MatterBinarySensor(MatterEntity, BinarySensorEntity): + """Representation of a Matter binary sensor.""" + + entity_description: MatterBinarySensorEntityDescription + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + self._attr_is_on = self._device_type_instance.get_cluster( + clusters.BooleanState + ).stateValue + + +class MatterOccupancySensor(MatterBinarySensor): + """Representation of a Matter occupancy sensor.""" + + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + occupancy = self._device_type_instance.get_cluster( + clusters.OccupancySensing + ).occupancy + # The first bit = if occupied + self._attr_is_on = occupancy & 1 == 1 + + +@dataclass +class MatterBinarySensorEntityDescription( + BinarySensorEntityDescription, + MatterEntityDescriptionBaseClass, +): + """Matter Binary Sensor entity description.""" + + +# You can't set default values on inherited data classes +MatterSensorEntityDescriptionFactory = partial( + MatterBinarySensorEntityDescription, entity_cls=MatterBinarySensor +) + +DEVICE_ENTITY: dict[ + type[device_types.DeviceType], + MatterEntityDescriptionBaseClass | list[MatterEntityDescriptionBaseClass], +] = { + device_types.ContactSensor: MatterSensorEntityDescriptionFactory( + key=device_types.ContactSensor, + name="Contact", + subscribe_attributes=(clusters.BooleanState.Attributes.StateValue,), + device_class=BinarySensorDeviceClass.DOOR, + ), + device_types.OccupancySensor: MatterSensorEntityDescriptionFactory( + key=device_types.OccupancySensor, + name="Occupancy", + entity_cls=MatterOccupancySensor, + subscribe_attributes=(clusters.OccupancySensing.Attributes.Occupancy,), + ), +} diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py new file mode 100644 index 00000000000..f570b0cf14c --- /dev/null +++ b/homeassistant/components/matter/config_flow.py @@ -0,0 +1,325 @@ +"""Config flow for Matter integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from matter_server.client import MatterClient +from matter_server.client.exceptions import CannotConnect, InvalidServerVersion +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + HassioServiceInfo, + is_hassio, +) +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.helpers import aiohttp_client + +from .addon import get_addon_manager +from .const import ( + ADDON_SLUG, + CONF_INTEGRATION_CREATED_ADDON, + CONF_USE_ADDON, + DOMAIN, + LOGGER, +) + +ADDON_SETUP_TIMEOUT = 5 +ADDON_SETUP_TIMEOUT_ROUNDS = 40 +DEFAULT_URL = "ws://localhost:5580/ws" +ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) + + +def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: + """Return a schema for the manual step.""" + default_url = user_input.get(CONF_URL, DEFAULT_URL) + return vol.Schema({vol.Required(CONF_URL, default=default_url): str}) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Validate the user input allows us to connect.""" + client = MatterClient(data[CONF_URL], aiohttp_client.async_get_clientsession(hass)) + await client.connect() + + +def build_ws_address(host: str, port: int) -> str: + """Return the websocket address.""" + return f"ws://{host}:{port}/ws" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Matter.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up flow instance.""" + self.ws_address: str | None = None + # If we install the add-on we should uninstall it on entry remove. + self.integration_created_addon = False + self.install_task: asyncio.Task | None = None + self.start_task: asyncio.Task | None = None + self.use_addon = False + + async def async_step_install_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Install Matter Server add-on.""" + 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" + ) + + try: + await self.install_task + except AddonError as err: + self.install_task = None + LOGGER.error(err) + return self.async_show_progress_done(next_step_id="install_failed") + + self.integration_created_addon = True + self.install_task = None + + return self.async_show_progress_done(next_step_id="start_addon") + + async def async_step_install_failed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Add-on installation failed.""" + return self.async_abort(reason="addon_install_failed") + + async def _async_install_addon(self) -> None: + """Install the Matter Server add-on.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + try: + await addon_manager.async_schedule_install_addon() + 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.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + try: + discovery_info_config = await addon_manager.async_get_addon_discovery_info() + except AddonError as err: + LOGGER.error(err) + raise AbortFlow("addon_get_discovery_info_failed") from err + + return discovery_info_config + + async def async_step_start_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Start Matter Server add-on.""" + if not self.start_task: + self.start_task = self.hass.async_create_task(self._async_start_addon()) + return self.async_show_progress( + step_id="start_addon", progress_action="start_addon" + ) + + try: + await self.start_task + except (CannotConnect, AddonError, AbortFlow) as err: + self.start_task = None + LOGGER.error(err) + return self.async_show_progress_done(next_step_id="start_failed") + + self.start_task = None + return self.async_show_progress_done(next_step_id="finish_addon_setup") + + async def async_step_start_failed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Add-on start failed.""" + return self.async_abort(reason="addon_start_failed") + + async def _async_start_addon(self) -> None: + """Start the Matter Server add-on.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + + try: + await addon_manager.async_schedule_start_addon() + # Sleep some seconds to let the add-on start properly before connecting. + for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS): + await asyncio.sleep(ADDON_SETUP_TIMEOUT) + try: + if not (ws_address := self.ws_address): + discovery_info = await self._async_get_addon_discovery_info() + ws_address = self.ws_address = build_ws_address( + discovery_info["host"], discovery_info["port"] + ) + await validate_input(self.hass, {CONF_URL: ws_address}) + except (AbortFlow, CannotConnect) as err: + LOGGER.debug( + "Add-on not ready yet, waiting %s seconds: %s", + ADDON_SETUP_TIMEOUT, + err, + ) + else: + break + else: + raise CannotConnect("Failed to start Matter Server add-on: timeout") + 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_info(self) -> AddonInfo: + """Return Matter Server add-on info.""" + addon_manager: AddonManager = get_addon_manager(self.hass) + try: + addon_info: AddonInfo = await addon_manager.async_get_addon_info() + except AddonError as err: + LOGGER.error(err) + raise AbortFlow("addon_info_failed") from err + + return addon_info + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if is_hassio(self.hass): + return await self.async_step_on_supervisor() + + return await self.async_step_manual() + + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a manual configuration.""" + if user_input is None: + return self.async_show_form( + step_id="manual", data_schema=get_manual_schema({}) + ) + + errors = {} + + try: + await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidServerVersion: + errors["base"] = "invalid_server_version" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.ws_address = user_input[CONF_URL] + + return await self._async_create_entry_or_abort() + + return self.async_show_form( + step_id="manual", data_schema=get_manual_schema(user_input), errors=errors + ) + + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + """Receive configuration from add-on discovery info. + + This flow is triggered by the Matter Server add-on. + """ + if discovery_info.slug != ADDON_SLUG: + return self.async_abort(reason="not_matter_addon") + + await self._async_handle_discovery_without_unique_id() + + self.ws_address = build_ws_address( + discovery_info.config["host"], discovery_info.config["port"] + ) + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """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") + + async def async_step_on_supervisor( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """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 + + addon_info = await self._async_get_addon_info() + + if addon_info.state == AddonState.RUNNING: + return await self.async_step_finish_addon_setup() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_start_addon() + + return await self.async_step_install_addon() + + async def async_step_finish_addon_setup( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Prepare info needed to complete the config entry.""" + if not self.ws_address: + discovery_info = await self._async_get_addon_discovery_info() + ws_address = self.ws_address = build_ws_address( + discovery_info["host"], discovery_info["port"] + ) + # Check that we can connect to the address. + try: + await validate_input(self.hass, {CONF_URL: ws_address}) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + + return await self._async_create_entry_or_abort() + + async def _async_create_entry_or_abort(self) -> FlowResult: + """Return a config entry for the flow or abort if already configured.""" + assert self.ws_address is not None + + if existing_config_entries := self._async_current_entries(): + config_entry = existing_config_entries[0] + self.hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + CONF_URL: self.ws_address, + CONF_USE_ADDON: self.use_addon, + CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, + }, + title=self.ws_address, + ) + await self.hass.config_entries.async_reload(config_entry.entry_id) + raise AbortFlow("reconfiguration_successful") + + # Abort any other flows that may be in progress + for progress in self._async_in_progress(): + self.hass.config_entries.flow.async_abort(progress["flow_id"]) + + return self.async_create_entry( + title=self.ws_address, + data={ + CONF_URL: self.ws_address, + CONF_USE_ADDON: self.use_addon, + CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, + }, + ) diff --git a/homeassistant/components/matter/const.py b/homeassistant/components/matter/const.py new file mode 100644 index 00000000000..c5ec1173ac0 --- /dev/null +++ b/homeassistant/components/matter/const.py @@ -0,0 +1,10 @@ +"""Constants for the Matter integration.""" +import logging + +ADDON_SLUG = "core_matter_server" + +CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" +CONF_USE_ADDON = "use_addon" + +DOMAIN = "matter" +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/matter/device_platform.py b/homeassistant/components/matter/device_platform.py new file mode 100644 index 00000000000..24e7f8b5dc4 --- /dev/null +++ b/homeassistant/components/matter/device_platform.py @@ -0,0 +1,30 @@ +"""All mappings of Matter devices to Home Assistant platforms.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.const import Platform + +from .binary_sensor import DEVICE_ENTITY as BINARY_SENSOR_DEVICE_ENTITY +from .light import DEVICE_ENTITY as LIGHT_DEVICE_ENTITY +from .sensor import DEVICE_ENTITY as SENSOR_DEVICE_ENTITY +from .switch import DEVICE_ENTITY as SWITCH_DEVICE_ENTITY + +if TYPE_CHECKING: + from matter_server.common.models.device_types import DeviceType + + from .entity import MatterEntityDescriptionBaseClass + + +DEVICE_PLATFORM: dict[ + Platform, + dict[ + type[DeviceType], + MatterEntityDescriptionBaseClass | list[MatterEntityDescriptionBaseClass], + ], +] = { + Platform.BINARY_SENSOR: BINARY_SENSOR_DEVICE_ENTITY, + Platform.LIGHT: LIGHT_DEVICE_ENTITY, + Platform.SENSOR: SENSOR_DEVICE_ENTITY, + Platform.SWITCH: SWITCH_DEVICE_ENTITY, +} diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py new file mode 100644 index 00000000000..019631750f4 --- /dev/null +++ b/homeassistant/components/matter/entity.py @@ -0,0 +1,118 @@ +"""Matter entity base class.""" +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import TYPE_CHECKING, Any + +from matter_server.common.models.device_type_instance import MatterDeviceTypeInstance +from matter_server.common.models.events import EventType +from matter_server.common.models.node_device import AbstractMatterNodeDevice + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .const import DOMAIN + +if TYPE_CHECKING: + from matter_server.client import MatterClient + from matter_server.common.models.node import MatterAttribute + +LOGGER = logging.getLogger(__name__) + + +@dataclass +class MatterEntityDescription: + """Mixin to map a matter device to a Home Assistant entity.""" + + entity_cls: type[MatterEntity] + subscribe_attributes: tuple + + +@dataclass +class MatterEntityDescriptionBaseClass(EntityDescription, MatterEntityDescription): + """For typing a base class that inherits from both entity descriptions.""" + + +class MatterEntity(Entity): + """Entity class for Matter devices.""" + + entity_description: MatterEntityDescriptionBaseClass + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + matter_client: MatterClient, + node_device: AbstractMatterNodeDevice, + device_type_instance: MatterDeviceTypeInstance, + entity_description: MatterEntityDescriptionBaseClass, + ) -> None: + """Initialize the entity.""" + self.matter_client = matter_client + self._node_device = node_device + self._device_type_instance = device_type_instance + self.entity_description = entity_description + node = device_type_instance.node + self._unsubscribes: list[Callable] = [] + # for fast lookups we create a mapping to the attribute paths + self._attributes_map: dict[type, str] = {} + self._attr_unique_id = f"{matter_client.server_info.compressed_fabric_id}-{node.unique_id}-{device_type_instance.endpoint}-{device_type_instance.device_type.device_type}" + + @property + def device_info(self) -> DeviceInfo | None: + """Return device info for device registry.""" + return {"identifiers": {(DOMAIN, self._node_device.device_info().uniqueID)}} + + async def async_added_to_hass(self) -> None: + """Handle being added to Home Assistant.""" + await super().async_added_to_hass() + + # Subscribe to attribute updates. + for attr_cls in self.entity_description.subscribe_attributes: + if matter_attr := self.get_matter_attribute(attr_cls): + self._attributes_map[attr_cls] = matter_attr.path + self._unsubscribes.append( + self.matter_client.subscribe( + self._on_matter_event, + EventType.ATTRIBUTE_UPDATED, + self._device_type_instance.node.node_id, + matter_attr.path, + ) + ) + continue + # not sure if this can happen, but just in case log it. + LOGGER.warning("Attribute not found on device: %s", attr_cls) + + # make sure to update the attributes once + self._update_from_device() + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + for unsub in self._unsubscribes: + unsub() + + @callback + def _on_matter_event(self, event: EventType, data: Any = None) -> None: + """Call on update.""" + self._update_from_device() + self.async_write_ha_state() + + @callback + @abstractmethod + def _update_from_device(self) -> None: + """Update data from Matter device.""" + + @callback + def get_matter_attribute(self, attribute: type) -> MatterAttribute | None: + """Lookup MatterAttribute instance on device instance by providing the attribute class.""" + return next( + ( + x + for x in self._device_type_instance.attributes + if x.attribute_type == attribute + ), + None, + ) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py new file mode 100644 index 00000000000..37454e3005c --- /dev/null +++ b/homeassistant/components/matter/light.py @@ -0,0 +1,173 @@ +"""Matter light.""" +from __future__ import annotations + +from dataclasses import dataclass +from functools import partial +from typing import TYPE_CHECKING, Any + +from chip.clusters import Objects as clusters +from matter_server.common.models import device_types + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ColorMode, + LightEntity, + LightEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import MatterEntity, MatterEntityDescriptionBaseClass +from .util import renormalize + +if TYPE_CHECKING: + from .adapter import MatterAdapter + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter Light from Config Entry.""" + matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id] + matter.register_platform_handler(Platform.LIGHT, async_add_entities) + + +class MatterLight(MatterEntity, LightEntity): + """Representation of a Matter light.""" + + entity_description: MatterLightEntityDescription + + def _supports_brightness(self) -> bool: + """Return if device supports brightness.""" + return ( + clusters.LevelControl.Attributes.CurrentLevel + in self.entity_description.subscribe_attributes + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn light on.""" + if ATTR_BRIGHTNESS not in kwargs or not self._supports_brightness(): + await self.matter_client.send_device_command( + node_id=self._device_type_instance.node.node_id, + endpoint=self._device_type_instance.endpoint, + command=clusters.OnOff.Commands.On(), + ) + return + + level_control = self._device_type_instance.get_cluster(clusters.LevelControl) + level = round( + renormalize( + kwargs[ATTR_BRIGHTNESS], + (0, 255), + (level_control.minLevel, level_control.maxLevel), + ) + ) + + await self.matter_client.send_device_command( + node_id=self._device_type_instance.node.node_id, + endpoint=self._device_type_instance.endpoint, + command=clusters.LevelControl.Commands.MoveToLevelWithOnOff( + level=level, + # It's required in TLV. We don't implement transition time yet. + transitionTime=0, + ), + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn light off.""" + await self.matter_client.send_device_command( + node_id=self._device_type_instance.node.node_id, + endpoint=self._device_type_instance.endpoint, + command=clusters.OnOff.Commands.Off(), + ) + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + if self._attr_supported_color_modes is None: + if self._supports_brightness(): + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + + if attr := self.get_matter_attribute(clusters.OnOff.Attributes.OnOff): + self._attr_is_on = attr.value + + if ( + clusters.LevelControl.Attributes.CurrentLevel + in self.entity_description.subscribe_attributes + ): + level_control = self._device_type_instance.get_cluster( + clusters.LevelControl + ) + + # Convert brightness to Home Assistant = 0..255 + self._attr_brightness = round( + renormalize( + level_control.currentLevel, + (level_control.minLevel, level_control.maxLevel), + (0, 255), + ) + ) + + +@dataclass +class MatterLightEntityDescription( + LightEntityDescription, + MatterEntityDescriptionBaseClass, +): + """Matter light entity description.""" + + +# You can't set default values on inherited data classes +MatterLightEntityDescriptionFactory = partial( + MatterLightEntityDescription, entity_cls=MatterLight +) + +# Mapping of a Matter Device type to Light Entity Description. +# A Matter device type (instance) can consist of multiple attributes. +# For example a Color Light which has an attribute to control brightness +# but also for color. + +DEVICE_ENTITY: dict[ + type[device_types.DeviceType], + MatterEntityDescriptionBaseClass | list[MatterEntityDescriptionBaseClass], +] = { + device_types.OnOffLight: MatterLightEntityDescriptionFactory( + key=device_types.OnOffLight, + subscribe_attributes=(clusters.OnOff.Attributes.OnOff,), + ), + device_types.DimmableLight: MatterLightEntityDescriptionFactory( + key=device_types.DimmableLight, + subscribe_attributes=( + clusters.OnOff.Attributes.OnOff, + clusters.LevelControl.Attributes.CurrentLevel, + ), + ), + device_types.DimmablePlugInUnit: MatterLightEntityDescriptionFactory( + key=device_types.DimmablePlugInUnit, + subscribe_attributes=( + clusters.OnOff.Attributes.OnOff, + clusters.LevelControl.Attributes.CurrentLevel, + ), + ), + device_types.ColorTemperatureLight: MatterLightEntityDescriptionFactory( + key=device_types.ColorTemperatureLight, + subscribe_attributes=( + clusters.OnOff.Attributes.OnOff, + clusters.LevelControl.Attributes.CurrentLevel, + clusters.ColorControl, + ), + ), + device_types.ExtendedColorLight: MatterLightEntityDescriptionFactory( + key=device_types.ExtendedColorLight, + subscribe_attributes=( + clusters.OnOff.Attributes.OnOff, + clusters.LevelControl.Attributes.CurrentLevel, + clusters.ColorControl, + ), + ), +} diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json new file mode 100644 index 00000000000..aa64ac4755e --- /dev/null +++ b/homeassistant/components/matter/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "matter", + "name": "Matter (BETA)", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/matter", + "requirements": ["python-matter-server==1.0.6"], + "dependencies": ["websocket_api"], + "codeowners": ["@MartinHjelmare", "@marcelveldt"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py new file mode 100644 index 00000000000..2b659fc1304 --- /dev/null +++ b/homeassistant/components/matter/sensor.py @@ -0,0 +1,167 @@ +"""Matter sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from functools import partial +from typing import TYPE_CHECKING, Any + +from chip.clusters import Objects as clusters +from chip.clusters.Types import Nullable, NullValue +from matter_server.common.models import device_types +from matter_server.common.models.device_type_instance import MatterDeviceTypeInstance + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + LIGHT_LUX, + PERCENTAGE, + VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, + Platform, + UnitOfPressure, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import MatterEntity, MatterEntityDescriptionBaseClass + +if TYPE_CHECKING: + from .adapter import MatterAdapter + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter sensors from Config Entry.""" + matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id] + matter.register_platform_handler(Platform.SENSOR, async_add_entities) + + +class MatterSensor(MatterEntity, SensorEntity): + """Representation of a Matter sensor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + entity_description: MatterSensorEntityDescription + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + measurement: Nullable | float | None + measurement = _get_attribute_value( + self._device_type_instance, + # We always subscribe to a single value + self.entity_description.subscribe_attributes[0], + ) + + if measurement is NullValue or measurement is None: + measurement = None + else: + measurement = self.entity_description.measurement_to_ha(measurement) + + self._attr_native_value = measurement + + +def _get_attribute_value( + device_type_instance: MatterDeviceTypeInstance, + attribute: clusters.ClusterAttributeDescriptor, +) -> Any: + """Return the value of an attribute.""" + # Find the cluster for this attribute. We don't have a lookup table yet. + cluster_cls: clusters.Cluster = next( + cluster + for cluster in device_type_instance.device_type.clusters + if cluster.id == attribute.cluster_id + ) + + # Find the attribute descriptor so we know the instance variable to fetch + attribute_descriptor: clusters.ClusterObjectFieldDescriptor = next( + descriptor + for descriptor in cluster_cls.descriptor.Fields + if descriptor.Tag == attribute.attribute_id + ) + + cluster_data = device_type_instance.get_cluster(cluster_cls) + return getattr(cluster_data, attribute_descriptor.Label) + + +@dataclass +class MatterSensorEntityDescriptionMixin: + """Required fields for sensor device mapping.""" + + measurement_to_ha: Callable[[float], float] + + +@dataclass +class MatterSensorEntityDescription( + SensorEntityDescription, + MatterEntityDescriptionBaseClass, + MatterSensorEntityDescriptionMixin, +): + """Matter Sensor entity description.""" + + +# You can't set default values on inherited data classes +MatterSensorEntityDescriptionFactory = partial( + MatterSensorEntityDescription, entity_cls=MatterSensor +) + + +DEVICE_ENTITY: dict[ + type[device_types.DeviceType], + MatterEntityDescriptionBaseClass | list[MatterEntityDescriptionBaseClass], +] = { + device_types.TemperatureSensor: MatterSensorEntityDescriptionFactory( + key=device_types.TemperatureSensor, + name="Temperature", + measurement_to_ha=lambda x: x / 100, + subscribe_attributes=( + clusters.TemperatureMeasurement.Attributes.MeasuredValue, + ), + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + device_types.PressureSensor: MatterSensorEntityDescriptionFactory( + key=device_types.PressureSensor, + name="Pressure", + measurement_to_ha=lambda x: x / 10, + subscribe_attributes=(clusters.PressureMeasurement.Attributes.MeasuredValue,), + native_unit_of_measurement=UnitOfPressure.KPA, + device_class=SensorDeviceClass.PRESSURE, + ), + device_types.FlowSensor: MatterSensorEntityDescriptionFactory( + key=device_types.FlowSensor, + name="Flow", + measurement_to_ha=lambda x: x / 10, + subscribe_attributes=(clusters.FlowMeasurement.Attributes.MeasuredValue,), + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, + ), + device_types.HumiditySensor: MatterSensorEntityDescriptionFactory( + key=device_types.HumiditySensor, + name="Humidity", + measurement_to_ha=lambda x: x / 100, + subscribe_attributes=( + clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue, + ), + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + ), + device_types.LightSensor: MatterSensorEntityDescriptionFactory( + key=device_types.LightSensor, + name="Light", + measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1), + subscribe_attributes=( + clusters.IlluminanceMeasurement.Attributes.MeasuredValue, + ), + native_unit_of_measurement=LIGHT_LUX, + device_class=SensorDeviceClass.ILLUMINANCE, + ), +} diff --git a/homeassistant/components/matter/services.yaml b/homeassistant/components/matter/services.yaml new file mode 100644 index 00000000000..18bb4be6452 --- /dev/null +++ b/homeassistant/components/matter/services.yaml @@ -0,0 +1,66 @@ +commission: + name: Commission device + description: > + Add a new device to your Matter network. + fields: + code: + name: Pairing code + description: The pairing code for the device. + required: true + selector: + text: +accept_shared_device: + name: Accept shared device + description: > + Add a shared device to your Matter network. + fields: + pin: + name: Pin code + description: The pin code for the device. + required: true + selector: + text: + +set_wifi: + name: Set Wi-Fi credentials + description: > + The Wi-Fi credentials will be sent as part of commissioning to a Matter device so it can connect to the Wi-Fi network. + fields: + ssid: + name: Network name + description: The SSID network name. + required: true + selector: + text: + password: + name: Password + description: The Wi-Fi network password. + required: true + selector: + text: + type: password +set_thread: + name: Set Thread network operational dataset + description: > + The Thread keys will be used as part of commissioning to let a Matter device join the Thread network. + + Get keys by running `ot-ctl dataset active -x` on the Open Thread Border Router. + fields: + thread_operation_dataset: + name: Thread Operational Dataset + description: The Thread Operational Dataset to set. + required: true + selector: + text: +open_commissioning_window: + name: Open Commissioning Window + description: > + Allow adding one of your devices to another Matter network by opening the commissioning window for this Matter device for 60 seconds. + fields: + device_id: + name: Device + description: The Matter device to add to the other Matter network. + required: true + selector: + device: + integration: matter diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json new file mode 100644 index 00000000000..594998c236f --- /dev/null +++ b/homeassistant/components/matter/strings.json @@ -0,0 +1,47 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "manual": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "on_supervisor": { + "title": "Select connection method", + "description": "Do you want to use the official Matter Server Supervisor add-on?\n\nIf you are already running the Matter Server in another add-on, in a custom container, natively etc., then do not select this option.", + "data": { + "use_addon": "Use the official Matter Server Supervisor add-on" + } + }, + "install_addon": { + "title": "The add-on installation has started" + }, + "start_addon": { + "title": "Starting add-on." + }, + "hassio_confirm": { + "title": "Set up the Matter integration with the Matter Server add-on" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_server_version": "The Matter server is not the correct version", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "addon_get_discovery_info_failed": "Failed to get Matter Server add-on discovery info.", + "addon_info_failed": "Failed to get Matter Server add-on info.", + "addon_install_failed": "Failed to install the Matter Server add-on.", + "addon_start_failed": "Failed to start the Matter Server add-on.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "not_matter_addon": "Discovered add-on is not the official Matter Server add-on.", + "reconfiguration_successful": "Successfully reconfigured the Matter integration." + }, + "progress": { + "install_addon": "Please wait while the Matter Server add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Matter Server add-on starts. This add-on is what powers Matter in Home Assistant. This may take some seconds." + } + } +} diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py new file mode 100644 index 00000000000..b7c2b999918 --- /dev/null +++ b/homeassistant/components/matter/switch.py @@ -0,0 +1,88 @@ +"""Matter switches.""" +from __future__ import annotations + +from dataclasses import dataclass +from functools import partial +from typing import TYPE_CHECKING, Any + +from chip.clusters import Objects as clusters +from matter_server.common.models import device_types + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import MatterEntity, MatterEntityDescriptionBaseClass + +if TYPE_CHECKING: + from .adapter import MatterAdapter + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter switches from Config Entry.""" + matter: MatterAdapter = hass.data[DOMAIN][config_entry.entry_id] + matter.register_platform_handler(Platform.SWITCH, async_add_entities) + + +class MatterSwitch(MatterEntity, SwitchEntity): + """Representation of a Matter switch.""" + + entity_description: MatterSwitchEntityDescription + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn switch on.""" + await self.matter_client.send_device_command( + node_id=self._device_type_instance.node.node_id, + endpoint=self._device_type_instance.endpoint, + command=clusters.OnOff.Commands.On(), + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn switch off.""" + await self.matter_client.send_device_command( + node_id=self._device_type_instance.node.node_id, + endpoint=self._device_type_instance.endpoint, + command=clusters.OnOff.Commands.Off(), + ) + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + self._attr_is_on = self._device_type_instance.get_cluster(clusters.OnOff).onOff + + +@dataclass +class MatterSwitchEntityDescription( + SwitchEntityDescription, + MatterEntityDescriptionBaseClass, +): + """Matter Switch entity description.""" + + +# You can't set default values on inherited data classes +MatterSwitchEntityDescriptionFactory = partial( + MatterSwitchEntityDescription, entity_cls=MatterSwitch +) + + +DEVICE_ENTITY: dict[ + type[device_types.DeviceType], + MatterEntityDescriptionBaseClass | list[MatterEntityDescriptionBaseClass], +] = { + device_types.OnOffPlugInUnit: MatterSwitchEntityDescriptionFactory( + key=device_types.OnOffPlugInUnit, + subscribe_attributes=(clusters.OnOff.Attributes.OnOff,), + device_class=SwitchDeviceClass.OUTLET, + ), +} diff --git a/homeassistant/components/matter/translations/en.json b/homeassistant/components/matter/translations/en.json new file mode 100644 index 00000000000..a812772142f --- /dev/null +++ b/homeassistant/components/matter/translations/en.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Failed to get Matter Server add-on discovery info.", + "addon_info_failed": "Failed to get Matter Server add-on info.", + "addon_install_failed": "Failed to install the Matter Server add-on.", + "addon_start_failed": "Failed to start the Matter Server add-on.", + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "not_matter_addon": "Discovered add-on is not the official Matter Server add-on.", + "reconfiguration_successful": "Successfully reconfigured the Matter integration." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_server_version": "The Matter server is not the correct version", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "progress": { + "install_addon": "Please wait while the Matter Server add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Matter Server add-on starts. This add-on is what powers Matter in Home Assistant. This may take some seconds." + }, + "step": { + "hassio_confirm": { + "title": "Set up the Matter integration with the Matter Server add-on" + }, + "install_addon": { + "title": "The add-on installation has started" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Use the official Matter Server Supervisor add-on" + }, + "description": "Do you want to use the official Matter Server Supervisor add-on?\n\nIf you are already running the Matter Server in another add-on, in a custom container, natively etc., then do not select this option.", + "title": "Select connection method" + }, + "start_addon": { + "title": "Starting add-on." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/matter/util.py b/homeassistant/components/matter/util.py new file mode 100644 index 00000000000..9f51ee0c0e6 --- /dev/null +++ b/homeassistant/components/matter/util.py @@ -0,0 +1,11 @@ +"""Provide integration utilities.""" +from __future__ import annotations + + +def renormalize( + number: float, from_range: tuple[float, float], to_range: tuple[float, float] +) -> float: + """Change value from from_range to to_range.""" + delta1 = from_range[1] - from_range[0] + delta2 = to_range[1] - to_range[0] + return (delta2 * (number - from_range[0]) / delta1) + to_range[0] diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index 946cff72f5b..67702ba5455 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -1,6 +1,5 @@ """Platform for Mazda device tracker integration.""" -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/mazda/translations/bg.json b/homeassistant/components/mazda/translations/bg.json index 1eb89184642..2cb991851ca 100644 --- a/homeassistant/components/mazda/translations/bg.json +++ b/homeassistant/components/mazda/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "account_locked": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d. \u041c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/mazda/translations/cs.json b/homeassistant/components/mazda/translations/cs.json index c769fdc28dd..7e01ff38a0a 100644 --- a/homeassistant/components/mazda/translations/cs.json +++ b/homeassistant/components/mazda/translations/cs.json @@ -5,6 +5,7 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "account_locked": "\u00da\u010det uzam\u010den. Pros\u00edm zkuste to znovu pozd\u011bji.", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" diff --git a/homeassistant/components/mazda/translations/he.json b/homeassistant/components/mazda/translations/he.json index 9856fb9034c..269b5952679 100644 --- a/homeassistant/components/mazda/translations/he.json +++ b/homeassistant/components/mazda/translations/he.json @@ -16,7 +16,7 @@ "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d1\u05d4\u05df \u05d0\u05ea\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 \u05db\u05d3\u05d9 \u05dc\u05d4\u05d9\u05db\u05e0\u05e1 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd MyMazda \u05dc\u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd." + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d1\u05d4\u05df \u05d4\u05d9\u05e0\u05da \u05de\u05e9\u05ea\u05de\u05e9 \u05db\u05d3\u05d9 \u05dc\u05d4\u05d9\u05db\u05e0\u05e1 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd MyMazda \u05dc\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd." } } } diff --git a/homeassistant/components/mazda/translations/sk.json b/homeassistant/components/mazda/translations/sk.json index f8b6dfeea81..9b00c29386f 100644 --- a/homeassistant/components/mazda/translations/sk.json +++ b/homeassistant/components/mazda/translations/sk.json @@ -1,15 +1,21 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "account_locked": "\u00da\u010det je zablokovan\u00fd. Sk\u00faste nesk\u00f4r pros\u00edm.", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo", + "region": "Oblas\u0165" } } } diff --git a/homeassistant/components/meater/translations/sk.json b/homeassistant/components/meater/translations/sk.json index a52d3b46e7c..e2216e824b3 100644 --- a/homeassistant/components/meater/translations/sk.json +++ b/homeassistant/components/meater/translations/sk.json @@ -1,7 +1,26 @@ { "config": { "error": { - "service_unavailable_error": "Rozhranie API je moment\u00e1lne nedostupn\u00e9, sk\u00faste to pros\u00edm nesk\u00f4r." + "invalid_auth": "Neplatn\u00e9 overenie", + "service_unavailable_error": "Rozhranie API je moment\u00e1lne nedostupn\u00e9, sk\u00faste to pros\u00edm nesk\u00f4r.", + "unknown_auth_error": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "data_description": { + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno slu\u017eby Meater Cloud, zvy\u010dajne e-mailov\u00e1 adresa." + }, + "description": "Nastavte si \u00fa\u010det Meater Cloud." + } } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 5771e1b6938..2337990933a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -491,8 +491,8 @@ class MediaPlayerEntity(Entity): _attr_sound_mode: str | None = None _attr_source_list: list[str] | None = None _attr_source: str | None = None - _attr_state: MediaPlayerState | str | None = None - _attr_supported_features: int = 0 + _attr_state: MediaPlayerState | None = None + _attr_supported_features: MediaPlayerEntityFeature = MediaPlayerEntityFeature(0) _attr_volume_level: float | None = None # Implement these for your media player @@ -506,7 +506,7 @@ class MediaPlayerEntity(Entity): return None @property - def state(self) -> MediaPlayerState | str | None: + def state(self) -> MediaPlayerState | None: """State of the player.""" return self._attr_state @@ -692,7 +692,7 @@ class MediaPlayerEntity(Entity): return self._attr_group_members @property - def supported_features(self) -> int: + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" return self._attr_supported_features @@ -920,9 +920,7 @@ class MediaPlayerEntity(Entity): async def async_toggle(self) -> None: """Toggle the power on the media player.""" if hasattr(self, "toggle"): - await self.hass.async_add_executor_job( - self.toggle # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.toggle) return if self.state in { @@ -940,9 +938,7 @@ class MediaPlayerEntity(Entity): This method is a coroutine. """ if hasattr(self, "volume_up"): - await self.hass.async_add_executor_job( - self.volume_up # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.volume_up) return if ( @@ -958,9 +954,7 @@ class MediaPlayerEntity(Entity): This method is a coroutine. """ if hasattr(self, "volume_down"): - await self.hass.async_add_executor_job( - self.volume_down # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.volume_down) return if ( @@ -973,9 +967,7 @@ class MediaPlayerEntity(Entity): async def async_media_play_pause(self) -> None: """Play or pause the media player.""" if hasattr(self, "media_play_pause"): - await self.hass.async_add_executor_job( - self.media_play_pause # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.media_play_pause) return if self.state == MediaPlayerState.PLAYING: @@ -1008,15 +1000,14 @@ class MediaPlayerEntity(Entity): @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" - supported_features = self.supported_features or 0 data: dict[str, Any] = {} - if supported_features & MediaPlayerEntityFeature.SELECT_SOURCE and ( + if self.supported_features & MediaPlayerEntityFeature.SELECT_SOURCE and ( source_list := self.source_list ): data[ATTR_INPUT_SOURCE_LIST] = source_list - if supported_features & MediaPlayerEntityFeature.SELECT_SOUND_MODE and ( + if self.supported_features & MediaPlayerEntityFeature.SELECT_SOUND_MODE and ( sound_mode_list := self.sound_mode_list ): data[ATTR_SOUND_MODE_LIST] = sound_mode_list diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index ea8069cc7e4..1cc90aa4904 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,5 +1,5 @@ """Provides the constants needed for component.""" -from enum import IntEnum +from enum import IntFlag from homeassistant.backports.enum import StrEnum @@ -176,7 +176,7 @@ REPEAT_MODE_ONE = "one" REPEAT_MODES = [REPEAT_MODE_OFF, REPEAT_MODE_ALL, REPEAT_MODE_ONE] -class MediaPlayerEntityFeature(IntEnum): +class MediaPlayerEntityFeature(IntFlag): """Supported features of the media player entity.""" PAUSE = 1 diff --git a/homeassistant/components/media_player/translations/de.json b/homeassistant/components/media_player/translations/de.json index d6468920628..4fc136e5743 100644 --- a/homeassistant/components/media_player/translations/de.json +++ b/homeassistant/components/media_player/translations/de.json @@ -2,7 +2,7 @@ "device_automation": { "condition_type": { "is_buffering": "{entity_name} puffert", - "is_idle": "{entity_name} ist unt\u00e4tig", + "is_idle": "{entity_name} ist inaktiv", "is_off": "{entity_name} ist ausgeschaltet", "is_on": "{entity_name} ist eingeschaltet", "is_paused": "{entity_name} ist pausiert", @@ -21,7 +21,7 @@ "state": { "_": { "buffering": "Puffern", - "idle": "Unt\u00e4tig", + "idle": "Inaktiv", "off": "Aus", "on": "An", "paused": "Pausiert", diff --git a/homeassistant/components/media_player/translations/is.json b/homeassistant/components/media_player/translations/is.json index 276e3428171..98148d50a0a 100644 --- a/homeassistant/components/media_player/translations/is.json +++ b/homeassistant/components/media_player/translations/is.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_buffering": "{entity_name} er a\u00f0 hla\u00f0a bi\u00f0minni", + "is_idle": "{entity_name} er a\u00f0ger\u00f0alaus", + "is_off": "{entity_name} er sl\u00f6kkt", + "is_on": "{entity_name} er \u00ed gangi", + "is_paused": "{entity_name} er \u00ed bi\u00f0", + "is_playing": "{entity_name} er a\u00f0 spila" + } + }, "state": { "_": { "idle": "A\u00f0ger\u00f0alaus", diff --git a/homeassistant/components/media_player/translations/sk.json b/homeassistant/components/media_player/translations/sk.json index 16818a4b593..a2d2813ea7b 100644 --- a/homeassistant/components/media_player/translations/sk.json +++ b/homeassistant/components/media_player/translations/sk.json @@ -1,4 +1,18 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} je ne\u010dinn\u00e9", + "is_off": "{entity_name} je vypnut\u00e9", + "is_on": "{entity_name} je zapnut\u00e9", + "is_paused": "{entity_name} je pozastaven\u00e9" + }, + "trigger_type": { + "changed_states": "{entity_name} zmenil stav", + "paused": "{entity_name} je pozastaven\u00e9", + "turned_off": "{entity_name} vypnut\u00e1", + "turned_on": "{entity_name} zapnut\u00e1" + } + }, "state": { "_": { "idle": "Ne\u010dinn\u00fd", diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index b5089c64b14..09e137c96d1 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -142,7 +142,7 @@ class MediaroomDevice(MediaPlayerEntity): State.UNKNOWN: None, } - self._state = state_map[mediaroom_state] + self._attr_state = state_map[mediaroom_state] def __init__(self, host, device_id, optimistic=False, timeout=DEFAULT_TIMEOUT): """Initialize the device.""" @@ -154,7 +154,7 @@ class MediaroomDevice(MediaPlayerEntity): ) self._channel = None self._optimistic = optimistic - self._state = ( + self._attr_state = ( MediaPlayerState.PLAYING if optimistic else MediaPlayerState.STANDBY ) self._name = f"Mediaroom {device_id if device_id else host}" @@ -179,7 +179,7 @@ class MediaroomDevice(MediaPlayerEntity): if not stb_state: return self.set_state(stb_state) - _LOGGER.debug("STB(%s) is [%s]", self.host, self._state) + _LOGGER.debug("STB(%s) is [%s]", self.host, self.state) self._available = True self.async_write_ha_state() @@ -215,7 +215,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd(command) if self._optimistic: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -231,11 +231,6 @@ class MediaroomDevice(MediaPlayerEntity): """Return the name of the device.""" return self._name - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def media_channel(self): """Channel currently playing.""" @@ -247,7 +242,7 @@ class MediaroomDevice(MediaPlayerEntity): try: self.set_state(await self.stb.turn_on()) if self._optimistic: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -259,7 +254,7 @@ class MediaroomDevice(MediaPlayerEntity): try: self.set_state(await self.stb.turn_off()) if self._optimistic: - self._state = MediaPlayerState.STANDBY + self._attr_state = MediaPlayerState.STANDBY self._available = True except PyMediaroomError: self._available = False @@ -272,7 +267,7 @@ class MediaroomDevice(MediaPlayerEntity): _LOGGER.debug("media_play()") await self.stb.send_cmd("PlayPause") if self._optimistic: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -284,7 +279,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("PlayPause") if self._optimistic: - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED self._available = True except PyMediaroomError: self._available = False @@ -296,7 +291,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("Stop") if self._optimistic: - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED self._available = True except PyMediaroomError: self._available = False @@ -308,7 +303,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("ProgDown") if self._optimistic: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -320,7 +315,7 @@ class MediaroomDevice(MediaPlayerEntity): try: await self.stb.send_cmd("ProgUp") if self._optimistic: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False diff --git a/homeassistant/components/melcloud/translations/sk.json b/homeassistant/components/melcloud/translations/sk.json index c043ef9ff19..708ebe26ec9 100644 --- a/homeassistant/components/melcloud/translations/sk.json +++ b/homeassistant/components/melcloud/translations/sk.json @@ -1,13 +1,21 @@ { "config": { + "abort": { + "already_configured": "Integr\u00e1cia MELCloud je pre tento e-mail u\u017e nakonfigurovan\u00e1. Pr\u00edstupov\u00fd token bol obnoven\u00fd." + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { + "password": "Heslo", "username": "Email" - } + }, + "description": "Pripojte sa pomocou svojho \u00fa\u010dtu MELCloud.", + "title": "Pripojte sa k MELCloud" } } } diff --git a/homeassistant/components/melnor/translations/it.json b/homeassistant/components/melnor/translations/it.json index 9124fdfce13..c4929c9c230 100644 --- a/homeassistant/components/melnor/translations/it.json +++ b/homeassistant/components/melnor/translations/it.json @@ -6,7 +6,7 @@ "step": { "bluetooth_confirm": { "description": "Vuoi aggiungere la valvola Bluetooth Melnor `{name}` a Home Assistant?", - "title": "Scoperta la valvola Bluetooth Melnor" + "title": "Rilevata valvola Bluetooth Melnor" } } } diff --git a/homeassistant/components/melnor/translations/sk.json b/homeassistant/components/melnor/translations/sk.json new file mode 100644 index 00000000000..50472089229 --- /dev/null +++ b/homeassistant/components/melnor/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V bl\u00edzkosti nie s\u00fa \u017eiadne zariadenia Melnor Bluetooth." + }, + "step": { + "bluetooth_confirm": { + "description": "Chcete prida\u0165 Melnor Bluetooth ventil `{name}` do Home Assistant?", + "title": "Objaven\u00fd Melnor Bluetooth ventil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 2aa70929795..222d4a040e3 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -20,10 +20,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType @@ -76,10 +76,10 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" _attr_has_entity_name = True - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__( self, diff --git a/homeassistant/components/met_eireann/translations/es.json b/homeassistant/components/met_eireann/translations/es.json index d476d15366e..1aae7e78022 100644 --- a/homeassistant/components/met_eireann/translations/es.json +++ b/homeassistant/components/met_eireann/translations/es.json @@ -11,7 +11,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Introduce tu ubicaci\u00f3n para usar los datos meteorol\u00f3gicos de la API del pron\u00f3stico meteorol\u00f3gico p\u00fablico de Met \u00c9ireann", + "description": "Introduce tu ubicaci\u00f3n para usar los datos meteorol\u00f3gicos de la API de la previsi\u00f3n meteorol\u00f3gica p\u00fablica de Met \u00c9ireann", "title": "Ubicaci\u00f3n" } } diff --git a/homeassistant/components/met_eireann/translations/sk.json b/homeassistant/components/met_eireann/translations/sk.json index 492ab052d9a..c631fc4b440 100644 --- a/homeassistant/components/met_eireann/translations/sk.json +++ b/homeassistant/components/met_eireann/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index b872e9a8df6..c4d8763efa7 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -11,10 +11,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType @@ -55,10 +55,10 @@ async def async_setup_entry( class MetEireannWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Éireann weather condition.""" - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index d0a713119af..4645d06d596 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -26,7 +26,7 @@ "step": { "init": { "data": { - "mode": "Modo de pron\u00f3stico" + "mode": "Modo de previsi\u00f3n" } } } diff --git a/homeassistant/components/meteo_france/translations/sk.json b/homeassistant/components/meteo_france/translations/sk.json new file mode 100644 index 00000000000..402d5f65bc7 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/sk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "cities": { + "data": { + "city": "Mesto" + }, + "description": "Vyberte si svoje mesto zo zoznamu" + }, + "user": { + "data": { + "city": "Mesto" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Re\u017eim predpovede" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 2727b4470c1..bf0ff8c469e 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -15,10 +15,10 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_MODE, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType @@ -77,10 +77,10 @@ async def async_setup_entry( class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__(self, coordinator: DataUpdateCoordinator, mode: str) -> None: """Initialise the platform with a data instance and station name.""" @@ -178,7 +178,7 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ) else: for forecast in self.coordinator.data.daily_forecast: - # stop when we don't have a weather condition (can happen around last days of forcast, max 14) + # stop when we don't have a weather condition (can happen around last days of forecast, max 14) if not forecast.get("weather12H"): break forecast_data.append( diff --git a/homeassistant/components/meteoclimatic/translations/he.json b/homeassistant/components/meteoclimatic/translations/he.json index db961a2f14c..71e6e6b6943 100644 --- a/homeassistant/components/meteoclimatic/translations/he.json +++ b/homeassistant/components/meteoclimatic/translations/he.json @@ -5,7 +5,7 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { - "not_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "not_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "step": { "user": { diff --git a/homeassistant/components/meteoclimatic/translations/sk.json b/homeassistant/components/meteoclimatic/translations/sk.json new file mode 100644 index 00000000000..67a321910ac --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "not_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "step": { + "user": { + "data": { + "code": "K\u00f3d stanice" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 8044dd04aa8..14b953663d0 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -3,7 +3,7 @@ from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -38,9 +38,9 @@ async def async_setup_entry( class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__(self, coordinator: DataUpdateCoordinator) -> None: """Initialise the weather platform.""" diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index ef9643be96a..3495f7b7c7a 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -51,7 +51,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="name", name="Station name", device_class=None, - native_unit_of_measurement=None, icon="mdi:label-outline", entity_registry_enabled_default=False, ), @@ -59,7 +58,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="weather", name="Weather", device_class=None, - native_unit_of_measurement=None, icon="mdi:weather-sunny", # but will adapt to current conditions entity_registry_enabled_default=True, ), @@ -91,7 +89,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wind_direction", name="Wind direction", - native_unit_of_measurement=None, icon="mdi:compass-outline", entity_registry_enabled_default=False, ), @@ -108,7 +105,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility", name="Visibility", device_class=None, - native_unit_of_measurement=None, icon="mdi:eye", entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/metoffice/translations/sk.json b/homeassistant/components/metoffice/translations/sk.json index abb3969f6b4..d1578eb3042 100644 --- a/homeassistant/components/metoffice/translations/sk.json +++ b/homeassistant/components/metoffice/translations/sk.json @@ -1,12 +1,20 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", "longitude": "Zemepisn\u00e1 d\u013a\u017eka" - } + }, + "description": "Zemepisn\u00e1 \u0161\u00edrka a d\u013a\u017eka sa pou\u017eije na n\u00e1jdenie najbli\u017e\u0161ej meteorologickej stanice." } } } diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 4733ca6ea73..8257c8a3c35 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -15,7 +15,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -82,9 +82,9 @@ class MetOfficeWeather( _attr_attribution = ATTRIBUTION _attr_has_entity_name = True - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_wind_speed_unit = UnitOfSpeed.MILES_PER_HOUR def __init__( self, diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index ec393125d24..4389c0d69b9 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -2,7 +2,7 @@ "domain": "microsoft", "name": "Microsoft Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/microsoft", - "requirements": ["pycsspeechtts==1.0.4"], + "requirements": ["pycsspeechtts==1.0.8"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["pycsspeechtts"] diff --git a/homeassistant/components/miflora/translations/sk.json b/homeassistant/components/miflora/translations/sk.json new file mode 100644 index 00000000000..ea1bb7d4d8f --- /dev/null +++ b/homeassistant/components/miflora/translations/sk.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "Integr\u00e1cia Mi Flora bola nahraden\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index cfa2e216d1d..2c3e480c297 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -3,8 +3,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER, SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER, + ScannerEntity, + SourceType, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry diff --git a/homeassistant/components/mikrotik/translations/bg.json b/homeassistant/components/mikrotik/translations/bg.json index 3316c8f5a6c..d9459868bdb 100644 --- a/homeassistant/components/mikrotik/translations/bg.json +++ b/homeassistant/components/mikrotik/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/mikrotik/translations/hy.json b/homeassistant/components/mikrotik/translations/hy.json new file mode 100644 index 00000000000..a3a5d6d7c2f --- /dev/null +++ b/homeassistant/components/mikrotik/translations/hy.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u0533\u0561\u0572\u057f\u0576\u0561\u0562\u0561\u057c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/sk.json b/homeassistant/components/mikrotik/translations/sk.json index 6f753f20966..82e54c59aac 100644 --- a/homeassistant/components/mikrotik/translations/sk.json +++ b/homeassistant/components/mikrotik/translations/sk.json @@ -1,13 +1,40 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "name_exists": "N\u00e1zov existuje" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Heslo pre {username} je neplatn\u00e9.", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov", - "port": "Port" + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Pou\u017eite ssl" + }, + "title": "Nastavte Mikrotik router" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Povoli\u0165 ARP ping" } } } diff --git a/homeassistant/components/mill/translations/sk.json b/homeassistant/components/mill/translations/sk.json new file mode 100644 index 00000000000..0331ed11690 --- /dev/null +++ b/homeassistant/components/mill/translations/sk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "cloud": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "local": { + "data": { + "ip_address": "IP adresa" + }, + "description": "Lok\u00e1lna IP adresa zariadenia." + }, + "user": { + "data": { + "connection_type": "Vyberte typ pripojenia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index 0fed67f15b9..b515608042f 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -6,12 +6,14 @@ from typing import Any, cast import voluptuous as vol +from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_TYPE from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaConfigFlowHandler, SchemaFlowFormStep, - SchemaFlowMenuStep, ) from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN @@ -23,13 +25,17 @@ _STATISTIC_MEASURES = [ selector.SelectOptionDict(value="median", label="Median"), selector.SelectOptionDict(value="last", label="Most recently updated"), selector.SelectOptionDict(value="range", label="Statistical range"), + selector.SelectOptionDict(value="sum", label="Sum"), ] OPTIONS_SCHEMA = vol.Schema( { vol.Required(CONF_ENTITY_IDS): selector.EntitySelector( - selector.EntitySelectorConfig(domain="sensor", multiple=True), + selector.EntitySelectorConfig( + domain=[SENSOR_DOMAIN, NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], + multiple=True, + ), ), vol.Required(CONF_TYPE): selector.SelectSelector( selector.SelectSelectorConfig(options=_STATISTIC_MEASURES), @@ -48,12 +54,12 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "user": SchemaFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA), } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), } diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index dd3e846aa84..788e259d4ec 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -3,8 +3,8 @@ "integration_type": "helper", "name": "Min/Max", "documentation": "https://www.home-assistant.io/integrations/min_max", - "codeowners": ["@fabaff"], + "codeowners": ["@gjohansson-ST"], "quality_scale": "internal", - "iot_class": "local_push", + "iot_class": "calculated", "config_flow": true } diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 87edcd38766..d0064d07511 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -1,8 +1,10 @@ """Support for displaying minimal, maximal, mean or median values.""" from __future__ import annotations +from datetime import datetime import logging import statistics +from typing import Any import voluptuous as vol @@ -20,12 +22,17 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + EventType, + StateType, +) from . import PLATFORMS from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN @@ -41,6 +48,7 @@ ATTR_MEDIAN = "median" ATTR_LAST = "last" ATTR_LAST_ENTITY_ID = "last_entity_id" ATTR_RANGE = "range" +ATTR_SUM = "sum" ICON = "mdi:calculator" @@ -51,6 +59,7 @@ SENSOR_TYPES = { ATTR_MEDIAN: "median", ATTR_LAST: "last", ATTR_RANGE: "range", + ATTR_SUM: "sum", } SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} @@ -62,7 +71,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENTITY_IDS): cv.entity_ids, vol.Optional(CONF_ROUND_DIGITS, default=2): vol.Coerce(int), - vol.Optional(CONF_UNIQUE_ID): str, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -100,10 +109,10 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the min/max/mean sensor.""" - entity_ids = config.get(CONF_ENTITY_IDS) - name = config.get(CONF_NAME) - sensor_type = config.get(CONF_TYPE) - round_digits = config.get(CONF_ROUND_DIGITS) + entity_ids: list[str] = config[CONF_ENTITY_IDS] + name: str | None = config.get(CONF_NAME) + sensor_type: str = config[CONF_TYPE] + round_digits: int = config[CONF_ROUND_DIGITS] unique_id = config.get(CONF_UNIQUE_ID) await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -113,10 +122,10 @@ async def async_setup_platform( ) -def calc_min(sensor_values): +def calc_min(sensor_values: list[tuple[str, Any]]) -> tuple[str | None, float | None]: """Calculate min value, honoring unknown states.""" - val = None - entity_id = None + val: float | None = None + entity_id: str | None = None for sensor_id, sensor_value in sensor_values: if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and ( val is None or val > sensor_value @@ -125,10 +134,10 @@ def calc_min(sensor_values): return entity_id, val -def calc_max(sensor_values): +def calc_max(sensor_values: list[tuple[str, Any]]) -> tuple[str | None, float | None]: """Calculate max value, honoring unknown states.""" - val = None - entity_id = None + val: float | None = None + entity_id: str | None = None for sensor_id, sensor_value in sensor_values: if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and ( val is None or val < sensor_value @@ -137,7 +146,7 @@ def calc_max(sensor_values): return entity_id, val -def calc_mean(sensor_values, round_digits): +def calc_mean(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None: """Calculate mean value, honoring unknown states.""" result = [ sensor_value @@ -147,10 +156,13 @@ def calc_mean(sensor_values, round_digits): if not result: return None - return round(statistics.mean(result), round_digits) + value: float = round(statistics.mean(result), round_digits) + return value -def calc_median(sensor_values, round_digits): +def calc_median( + sensor_values: list[tuple[str, Any]], round_digits: int +) -> float | None: """Calculate median value, honoring unknown states.""" result = [ sensor_value @@ -160,10 +172,11 @@ def calc_median(sensor_values, round_digits): if not result: return None - return round(statistics.median(result), round_digits) + value: float = round(statistics.median(result), round_digits) + return value -def calc_range(sensor_values, round_digits): +def calc_range(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None: """Calculate range value, honoring unknown states.""" result = [ sensor_value @@ -173,7 +186,20 @@ def calc_range(sensor_values, round_digits): if not result: return None - return round(max(result) - min(result), round_digits) + value: float = round(max(result) - min(result), round_digits) + return value + + +def calc_sum(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None: + """Calculate a sum of values, not honoring unknown states.""" + result = 0 + for _, sensor_value in sensor_values: + if sensor_value in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + return None + result += sensor_value + + value: float = round(result, round_digits) + return value class MinMaxSensor(SensorEntity): @@ -183,7 +209,14 @@ class MinMaxSensor(SensorEntity): _attr_should_poll = False _attr_state_class = SensorStateClass.MEASUREMENT - def __init__(self, entity_ids, name, sensor_type, round_digits, unique_id): + def __init__( + self, + entity_ids: list[str], + name: str | None, + sensor_type: str, + round_digits: int, + unique_id: str | None, + ) -> None: """Initialize the min/max sensor.""" self._attr_unique_id = unique_id self._entity_ids = entity_ids @@ -197,11 +230,18 @@ class MinMaxSensor(SensorEntity): self._sensor_attr = SENSOR_TYPE_TO_ATTR[self._sensor_type] self._unit_of_measurement = None self._unit_of_measurement_mismatch = False - self.min_value = self.max_value = self.mean = self.last = self.median = None - self.range = None - self.min_entity_id = self.max_entity_id = self.last_entity_id = None + self.min_value: float | None = None + self.max_value: float | None = None + self.mean: float | None = None + self.last: float | None = None + self.median: float | None = None + self.range: float | None = None + self.sum: float | None = None + self.min_entity_id: str | None = None + self.max_entity_id: str | None = None + self.last_entity_id: str | None = None self.count_sensors = len(self._entity_ids) - self.states = {} + self.states: dict[str, Any] = {} async def async_added_to_hass(self) -> None: """Handle added to Hass.""" @@ -220,21 +260,22 @@ class MinMaxSensor(SensorEntity): self._calc_values() @property - def native_value(self): + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" if self._unit_of_measurement_mismatch: return None - return getattr(self, self._sensor_attr) + value: StateType | datetime = getattr(self, self._sensor_attr) + return value @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if self._unit_of_measurement_mismatch: return "ERR" return self._unit_of_measurement @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the sensor.""" if self._sensor_type == "min": return {ATTR_MIN_ENTITY_ID: self.min_entity_id} @@ -245,10 +286,12 @@ class MinMaxSensor(SensorEntity): return None @callback - def _async_min_max_sensor_state_listener(self, event, update_state=True): + def _async_min_max_sensor_state_listener( + self, event: EventType, update_state: bool = True + ) -> None: """Handle the sensor state changes.""" - new_state = event.data.get("new_state") - entity = event.data.get("entity_id") + new_state: State | None = event.data.get("new_state") + entity: str = event.data["entity_id"] if ( new_state is None @@ -296,7 +339,7 @@ class MinMaxSensor(SensorEntity): self.async_write_ha_state() @callback - def _calc_values(self): + def _calc_values(self) -> None: """Calculate the values.""" sensor_values = [ (entity_id, self.states[entity_id]) @@ -308,3 +351,4 @@ class MinMaxSensor(SensorEntity): self.mean = calc_mean(sensor_values, self._round_digits) self.median = calc_median(sensor_values, self._round_digits) self.range = calc_range(sensor_values, self._round_digits) + self.sum = calc_sum(sensor_values, self._round_digits) diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json index 596dd2250eb..67e8416bc2c 100644 --- a/homeassistant/components/min_max/strings.json +++ b/homeassistant/components/min_max/strings.json @@ -1,10 +1,10 @@ { - "title": "Min / max / mean / median sensor", + "title": "Combine the state of several sensors", "config": { "step": { "user": { - "title": "Add min / max / mean / median sensor", - "description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.", + "title": "Combine the state of several sensors", + "description": "Create a sensor that calculates a min, max, mean, median or sum from a list of input sensors.", "data": { "entity_ids": "Input entities", "name": "Name", @@ -12,7 +12,7 @@ "type": "Statistic characteristic" }, "data_description": { - "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." + "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean, median or sum." } } } diff --git a/homeassistant/components/min_max/translations/de.json b/homeassistant/components/min_max/translations/de.json index cb4ad7b9996..81003aed00e 100644 --- a/homeassistant/components/min_max/translations/de.json +++ b/homeassistant/components/min_max/translations/de.json @@ -9,10 +9,10 @@ "type": "Statistisches Merkmal" }, "data_description": { - "round_digits": "Steuert die Anzahl der Dezimalstellen in der Ausgabe, wenn das Statistikmerkmal Mittelwert oder Median ist." + "round_digits": "Steuert die Anzahl der Dezimalstellen in der Ausgabe, wenn das Statistikmerkmal Mittelwert, Median oder Summe ist." }, - "description": "Erstelle einen Sensor, der einen Mindest-, H\u00f6chst-, Mittel- oder Medianwert aus einer Liste von Eingabesensoren berechnet.", - "title": "Min/Max/Mittelwert/Median-Sensor hinzuf\u00fcgen" + "description": "Erstelle einen Sensor, der einen Mindest-, H\u00f6chst-, Mittelwert, Median oder eine Summe aus einer Liste von Eingabesensoren berechnet.", + "title": "Kombiniere den Zustand mehrerer Sensoren" } } }, @@ -25,10 +25,10 @@ "type": "Statistisches Merkmal" }, "data_description": { - "round_digits": "Steuert die Anzahl der Dezimalstellen in der Ausgabe, wenn das Statistikmerkmal Mittelwert oder Median ist." + "round_digits": "Steuert die Anzahl der Dezimalstellen in der Ausgabe, wenn das Statistikmerkmal Mittelwert, Median oder Summe ist." } } } }, - "title": "Min / Max / Mittelwert / Mediansensor" + "title": "Kombiniere den Zustand mehrerer Sensoren" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/en.json b/homeassistant/components/min_max/translations/en.json index 8cc0d41c419..6c661c980c0 100644 --- a/homeassistant/components/min_max/translations/en.json +++ b/homeassistant/components/min_max/translations/en.json @@ -9,10 +9,10 @@ "type": "Statistic characteristic" }, "data_description": { - "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." + "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean, median or sum." }, - "description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.", - "title": "Add min / max / mean / median sensor" + "description": "Create a sensor that calculates a min, max, mean, median or sum from a list of input sensors.", + "title": "Combine the state of several sensors" } } }, @@ -25,10 +25,10 @@ "type": "Statistic characteristic" }, "data_description": { - "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." + "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean, median or sum." } } } }, - "title": "Min / max / mean / median sensor" + "title": "Combine the state of several sensors" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json index 149f3f030d3..cd2d9295189 100644 --- a/homeassistant/components/min_max/translations/es.json +++ b/homeassistant/components/min_max/translations/es.json @@ -9,10 +9,10 @@ "type": "Caracter\u00edstica estad\u00edstica" }, "data_description": { - "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media, mediana o suma." }, - "description": "Crea un sensor que calcula el valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", - "title": "A\u00f1adir sensor m\u00edn / m\u00e1x / media / mediana" + "description": "Crea un sensor que calcule un m\u00ednimo, un m\u00e1ximo, una media, una mediana o una suma a partir de una lista de sensores de entrada.", + "title": "Combinar el estado de varios sensores" } } }, @@ -25,10 +25,10 @@ "type": "Caracter\u00edstica estad\u00edstica" }, "data_description": { - "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media, mediana o suma." } } } }, - "title": "Sensor m\u00edn / m\u00e1x / media / mediana" + "title": "Combinar el estado de varios sensores" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/et.json b/homeassistant/components/min_max/translations/et.json index c57b4a055a8..e0b9e561aaf 100644 --- a/homeassistant/components/min_max/translations/et.json +++ b/homeassistant/components/min_max/translations/et.json @@ -9,10 +9,10 @@ "type": "Statistiline tunnus" }, "data_description": { - "round_digits": "M\u00e4\u00e4rab k\u00fcmnendkohtade arvu v\u00e4ljundis kui statistika tunnus on keskmine v\u00f5i mediaan." + "round_digits": "M\u00e4\u00e4rab k\u00fcmnendkohtade arvu v\u00e4ljundis kui statistika tunnus on keskmine, summa v\u00f5i mediaan." }, - "description": "Loo andur mis arvutab sisendandurite loendist minimaalse, maksimaalse, keskmise v\u00f5i mediaanv\u00e4\u00e4rtuse.", - "title": "Lisa min / max / keskmine / mediaanandur" + "description": "Loo andur mis arvutab sisendandurite loendist minimaalse, maksimaalse, keskmise, summa v\u00f5i mediaanv\u00e4\u00e4rtuse.", + "title": "\u00dchenda mitme anduri olek" } } }, @@ -30,5 +30,5 @@ } } }, - "title": "Min / max / keskmine / mediaanandur" + "title": "\u00dchenda mitme anduri olek" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/id.json b/homeassistant/components/min_max/translations/id.json index 9e06b47de95..8ea76d6f73c 100644 --- a/homeassistant/components/min_max/translations/id.json +++ b/homeassistant/components/min_max/translations/id.json @@ -9,10 +9,10 @@ "type": "Karakteristik statistik" }, "data_description": { - "round_digits": "Mengontrol jumlah digit desimal dalam output ketika karakteristik statistik adalah rata-rata atau median." + "round_digits": "Mengontrol jumlah digit desimal dalam output ketika karakteristik statistik adalah rata-rata, median, atau jumlah." }, - "description": "Buat sensor yang menghitung nilai min, maks, rata-rata, atau median dari sejumlah sensor input.", - "title": "Tambahkan sensor min/maks/rata-rata/median" + "description": "Buat sensor yang menghitung nilai min, maks, rata-rata, median, atau jumlah dari sejumlah sensor input.", + "title": "Gabungkan status beberapa sensor" } } }, @@ -25,10 +25,10 @@ "type": "Karakteristik statistik" }, "data_description": { - "round_digits": "Mengontrol jumlah digit desimal dalam output ketika karakteristik statistik adalah rata-rata atau median." + "round_digits": "Mengontrol jumlah digit desimal dalam output ketika karakteristik statistik adalah rata-rata, median, atau jumlah." } } } }, - "title": "Sensor min/maks/rata-rata/median" + "title": "Gabungkan status beberapa sensor" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/no.json b/homeassistant/components/min_max/translations/no.json index 5c9391b374f..7dab46b4ac1 100644 --- a/homeassistant/components/min_max/translations/no.json +++ b/homeassistant/components/min_max/translations/no.json @@ -9,10 +9,10 @@ "type": "Statistisk karakteristikk" }, "data_description": { - "round_digits": "Styrer antall desimaler i utdata n\u00e5r statistikkkarakteristikken er gjennomsnitt eller median." + "round_digits": "Styrer antall desimaler i utdata n\u00e5r statistikkkarakteristikken er gjennomsnitt, median eller sum." }, - "description": "Lag en sensor som beregner en min, maks, middelverdi eller medianverdi fra en liste over inngangssensorer.", - "title": "Legg til min / maks / gjennomsnitt / median sensor" + "description": "Lag en sensor som beregner en min, maks, gjennomsnitt, median eller sum fra en liste over inngangssensorer.", + "title": "Kombiner tilstanden til flere sensorer" } } }, @@ -25,10 +25,10 @@ "type": "Statistisk karakteristikk" }, "data_description": { - "round_digits": "Styrer antall desimaler i utdata n\u00e5r statistikkkarakteristikken er gjennomsnitt eller median." + "round_digits": "Styrer antall desimaler i utdata n\u00e5r statistikkkarakteristikken er gjennomsnitt, median eller sum." } } } }, - "title": "Min / maks / gjennomsnitt / median sensor" + "title": "Kombiner tilstanden til flere sensorer" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/pt-BR.json b/homeassistant/components/min_max/translations/pt-BR.json index 5a2c9a85d4f..49a3a018058 100644 --- a/homeassistant/components/min_max/translations/pt-BR.json +++ b/homeassistant/components/min_max/translations/pt-BR.json @@ -9,10 +9,10 @@ "type": "Caracter\u00edstica estat\u00edstica" }, "data_description": { - "round_digits": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda quando a caracter\u00edstica estat\u00edstica \u00e9 m\u00e9dia ou mediana." + "round_digits": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda quando a caracter\u00edstica estat\u00edstica \u00e9 m\u00e9dia, mediana ou soma." }, - "description": "Crie um sensor que calcula um valor m\u00ednimo, m\u00e1ximo, m\u00e9dio ou mediano de uma lista de sensores de entrada.", - "title": "Adicionar sensor de m\u00ednima / m\u00e1xima / m\u00e9dia / mediana" + "description": "Crie um sensor que calcule um m\u00ednimo, m\u00e1ximo, m\u00e9dia, mediana ou soma de uma lista de sensores de entrada.", + "title": "Combine o estado de v\u00e1rios sensores" } } }, @@ -25,10 +25,10 @@ "type": "Caracter\u00edstica estat\u00edstica" }, "data_description": { - "round_digits": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda quando a caracter\u00edstica estat\u00edstica \u00e9 m\u00e9dia ou mediana." + "round_digits": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda quando a caracter\u00edstica estat\u00edstica \u00e9 m\u00e9dia, mediana ou soma." } } } }, - "title": "Sensor m\u00edn./m\u00e1x./m\u00e9dio/mediano" + "title": "Combine o estado de v\u00e1rios sensores" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/ru.json b/homeassistant/components/min_max/translations/ru.json index 0fdd9198a72..dab8747e1ae 100644 --- a/homeassistant/components/min_max/translations/ru.json +++ b/homeassistant/components/min_max/translations/ru.json @@ -9,10 +9,10 @@ "type": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430" }, "data_description": { - "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439." + "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439, \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439 \u0438\u043b\u0438 \u0441\u0443\u043c\u043c\u043e\u0439." }, - "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u0441\u0440\u0435\u0434\u043d\u0435\u0435 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432.", - "title": "\u041c\u0438\u043d\u0438\u043c\u0443\u043c / \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c / \u0441\u0440\u0435\u0434\u043d\u0435\u0435 / \u043c\u0435\u0434\u0438\u0430\u043d\u0430" + "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0441\u0443\u043c\u043c\u0443, \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u0441\u0440\u0435\u0434\u043d\u0435\u0435 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432.", + "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" } } }, @@ -25,10 +25,10 @@ "type": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430" }, "data_description": { - "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439." + "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439, \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439 \u0438\u043b\u0438 \u0441\u0443\u043c\u043c\u043e\u0439." } } } }, - "title": "\u041d\u043e\u0432\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440" + "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/sk.json b/homeassistant/components/min_max/translations/sk.json new file mode 100644 index 00000000000..18f720b8c2b --- /dev/null +++ b/homeassistant/components/min_max/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Vstupn\u00e9 entity", + "name": "N\u00e1zov", + "round_digits": "Presnos\u0165" + }, + "title": "Skombinujte stav nieko\u013ek\u00fdch sn\u00edma\u010dov" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Vstupn\u00e9 entity", + "round_digits": "Presnos\u0165" + } + } + } + }, + "title": "Skombinujte stav nieko\u013ek\u00fdch sn\u00edma\u010dov" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/zh-Hant.json b/homeassistant/components/min_max/translations/zh-Hant.json index abdf3fe737f..628e2458c65 100644 --- a/homeassistant/components/min_max/translations/zh-Hant.json +++ b/homeassistant/components/min_max/translations/zh-Hant.json @@ -9,10 +9,10 @@ "type": "\u7d71\u8a08\u7279\u5fb5" }, "data_description": { - "round_digits": "\u7576\u7d71\u8a08\u7279\u5fb5\u70ba\u5e73\u5747\u503c\u6216\u4e2d\u503c\u6642\u3001\u63a7\u5236\u8f38\u51fa\u5c0f\u6578\u4f4d\u6578\u3002" + "round_digits": "\u7576\u7d71\u8a08\u7279\u5fb5\u70ba\u5e73\u5747\u503c\u3001\u4e2d\u503c\u6216\u7e3d\u5408\u6642\u3001\u63a7\u5236\u8f38\u51fa\u5c0f\u6578\u4f4d\u6578\u3002" }, - "description": "\u65b0\u589e\u81ea\u8f38\u5165\u611f\u6e2c\u5668\u4e86\u8868\u4e2d\uff0c\u8a08\u7b97\u6700\u4f4e\u3001\u6700\u9ad8\u3001\u5e73\u5747\u503c\u6216\u4e2d\u503c\u611f\u6e2c\u5668\u3002", - "title": "\u65b0\u589e\u6700\u5c0f\u503c / \u6700\u5927\u503c / \u5e73\u5747\u503c / \u4e2d\u503c\u611f\u6e2c\u5668" + "description": "\u65b0\u589e\u81ea\u8f38\u5165\u611f\u6e2c\u5668\u4e86\u8868\u4e2d\uff0c\u8a08\u7b97\u6700\u4f4e\u3001\u6700\u9ad8\u3001\u5e73\u5747\u503c\u3001\u4e2d\u503c\u6216\u7e3d\u5408\u611f\u6e2c\u5668\u3002", + "title": "\u7d50\u5408\u591a\u500b\u611f\u6e2c\u5668\u72c0\u614b" } } }, @@ -25,10 +25,10 @@ "type": "\u7d71\u8a08\u7279\u5fb5" }, "data_description": { - "round_digits": "\u7576\u7d71\u8a08\u7279\u5fb5\u70ba\u5e73\u5747\u503c\u6216\u4e2d\u503c\u6642\u3001\u63a7\u5236\u8f38\u51fa\u5c0f\u6578\u4f4d\u6578\u3002" + "round_digits": "\u7576\u7d71\u8a08\u7279\u5fb5\u70ba\u5e73\u5747\u503c\u3001\u4e2d\u503c\u6216\u7e3d\u5408\u6642\u3001\u63a7\u5236\u8f38\u51fa\u5c0f\u6578\u4f4d\u6578\u3002" } } } }, - "title": "\u6700\u5c0f\u503c / \u6700\u5927\u503c / \u5e73\u5747\u503c / \u4e2d\u503c\u611f\u6e2c\u5668" + "title": "\u7d50\u5408\u591a\u500b\u611f\u6e2c\u5668\u72c0\u614b" } \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/sk.json b/homeassistant/components/minecraft_server/translations/sk.json index af15f92c2f2..6012400f204 100644 --- a/homeassistant/components/minecraft_server/translations/sk.json +++ b/homeassistant/components/minecraft_server/translations/sk.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165 k serveru. Skontrolujte hostite\u013ea a port a sk\u00faste to znova. Tie\u017e sa uistite, \u017ee na svojom serveri pou\u017e\u00edvate aspo\u0148 Minecraft verzie 1.7.", + "invalid_ip": "Adresa IP je neplatn\u00e1 (adresu MAC sa nepodarilo ur\u010di\u0165). Opravte to a sk\u00faste to znova.", + "invalid_port": "Port mus\u00ed by\u0165 v rozsahu od 1024 do 65535. Opravte ho a sk\u00faste to znova." + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov" - } + }, + "description": "Nastavte in\u0161tanciu servera Minecraft tak, aby umo\u017e\u0148ovala monitorovanie.", + "title": "Prepojenie servera Minecraft" } } } diff --git a/homeassistant/components/mjpeg/translations/bg.json b/homeassistant/components/mjpeg/translations/bg.json index 0e88f508191..19b62c42896 100644 --- a/homeassistant/components/mjpeg/translations/bg.json +++ b/homeassistant/components/mjpeg/translations/bg.json @@ -13,6 +13,7 @@ "mjpeg_url": "MJPEG URL", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "still_image_url": "URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } @@ -30,6 +31,7 @@ "mjpeg_url": "MJPEG URL", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "still_image_url": "URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/mjpeg/translations/sk.json b/homeassistant/components/mjpeg/translations/sk.json index 4a2050c2353..d669ecdfa1c 100644 --- a/homeassistant/components/mjpeg/translations/sk.json +++ b/homeassistant/components/mjpeg/translations/sk.json @@ -1,24 +1,38 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "user": { "data": { - "name": "N\u00e1zov" + "mjpeg_url": "MJPEG URL", + "name": "N\u00e1zov", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } } }, "options": { "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "init": { "data": { - "name": "N\u00e1zov" + "mjpeg_url": "MJPEG URL", + "name": "N\u00e1zov", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } } diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index c5e02a38dcd..29133e11283 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from moat_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from moat_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -20,17 +20,14 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN @@ -79,27 +76,13 @@ def _device_key_to_bluetooth_entity_key( return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to hass device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/moat/translations/he.json b/homeassistant/components/moat/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/moat/translations/he.json +++ b/homeassistant/components/moat/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/moat/translations/sk.json b/homeassistant/components/moat/translations/sk.json new file mode 100644 index 00000000000..b121bbc35a3 --- /dev/null +++ b/homeassistant/components/moat/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 6f9c68224ec..d347a0cc4db 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -5,8 +5,8 @@ from homeassistant.components.device_tracker import ( ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, SourceType, + TrackerEntity, ) -from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index a116e6196f4..25217962bc8 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -1,10 +1,11 @@ """Helpers for mobile_app.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from http import HTTPStatus import json import logging +from typing import Any from aiohttp.web import Response, json_response from nacl.encoding import Base64Encoder, HexEncoder, RawEncoder @@ -111,7 +112,7 @@ def _decrypt_payload_legacy(key: str | None, ciphertext: str) -> dict[str, str] return _decrypt_payload_helper(key, ciphertext, get_key_bytes, RawEncoder) -def registration_context(registration: dict) -> Context: +def registration_context(registration: Mapping[str, Any]) -> Context: """Generate a context from a request.""" return Context(user_id=registration[CONF_USER_ID]) @@ -173,11 +174,11 @@ def savable_state(hass: HomeAssistant) -> dict: def webhook_response( - data, + data: Any, *, - registration: dict, + registration: Mapping[str, Any], status: HTTPStatus = HTTPStatus.OK, - headers: dict | None = None, + headers: Mapping[str, str] | None = None, ) -> Response: """Return a encrypted response if registration supports it.""" data = json.dumps(data, cls=JSONEncoder) diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index ea8c56d1a7c..3c34a291df1 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -91,8 +91,9 @@ class RegistrationsView(HomeAssistantView): ) remote_ui_url = None - with suppress(hass.components.cloud.CloudNotAvailable): - remote_ui_url = cloud.async_remote_ui_url(hass) + if cloud.async_active_subscription(hass): + with suppress(hass.components.cloud.CloudNotAvailable): + remote_ui_url = cloud.async_remote_ui_url(hass) return self.json( { diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7c6bcc58db9..2dd578a3fea 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,10 +1,14 @@ """Webhook handlers for mobile_app.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable, Coroutine from contextlib import suppress from functools import wraps from http import HTTPStatus import logging import secrets +from typing import Any from aiohttp.web import HTTPBadRequest, Request, Response, json_response from nacl.exceptions import CryptoError @@ -28,6 +32,7 @@ from homeassistant.components.sensor import ( STATE_CLASSES as SENSOSR_STATE_CLASSES, ) from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_DOMAIN, @@ -39,7 +44,7 @@ from homeassistant.const import ( CONF_WEBHOOK_ID, ) from homeassistant.core import EventOrigin, HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceNotFound +from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -90,6 +95,7 @@ from .const import ( CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, + DATA_DEVICES, DOMAIN, ERR_ENCRYPTION_ALREADY_ENABLED, ERR_ENCRYPTION_NOT_AVAILABLE, @@ -115,7 +121,9 @@ _LOGGER = logging.getLogger(__name__) DELAY_SAVE = 10 -WEBHOOK_COMMANDS = Registry() # type: ignore[var-annotated] +WEBHOOK_COMMANDS: Registry[ + str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]] +] = Registry() COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] @@ -162,9 +170,9 @@ async def handle_webhook( if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: return Response(status=410) - config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + config_entry: ConfigEntry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] - device_name = config_entry.data[ATTR_DEVICE_NAME] + device_name: str = config_entry.data[ATTR_DEVICE_NAME] try: req_data = await request.json() @@ -246,7 +254,9 @@ async def handle_webhook( vol.Optional(ATTR_SERVICE_DATA, default={}): dict, } ) -async def webhook_call_service(hass, config_entry, data): +async def webhook_call_service( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: """Handle a call service webhook.""" try: await hass.services.async_call( @@ -275,9 +285,11 @@ async def webhook_call_service(hass, config_entry, data): vol.Optional(ATTR_EVENT_DATA, default={}): dict, } ) -async def webhook_fire_event(hass, config_entry, data): +async def webhook_fire_event( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: """Handle a fire event webhook.""" - event_type = data[ATTR_EVENT_TYPE] + event_type: str = data[ATTR_EVENT_TYPE] hass.bus.async_fire( event_type, data[ATTR_EVENT_DATA], @@ -289,7 +301,9 @@ async def webhook_fire_event(hass, config_entry, data): @WEBHOOK_COMMANDS.register("stream_camera") @validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string}) -async def webhook_stream_camera(hass, config_entry, data): +async def webhook_stream_camera( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, str] +) -> Response: """Handle a request to HLS-stream a camera.""" if (camera_state := hass.states.get(data[ATTR_CAMERA_ENTITY_ID])) is None: return webhook_response( @@ -298,7 +312,9 @@ async def webhook_stream_camera(hass, config_entry, data): status=HTTPStatus.BAD_REQUEST, ) - resp = {"mjpeg_path": f"/api/camera_proxy_stream/{camera_state.entity_id}"} + resp: dict[str, Any] = { + "mjpeg_path": f"/api/camera_proxy_stream/{camera_state.entity_id}" + } if camera_state.attributes[ATTR_SUPPORTED_FEATURES] & CameraEntityFeature.STREAM: try: @@ -322,14 +338,16 @@ async def webhook_stream_camera(hass, config_entry, data): } } ) -async def webhook_render_template(hass, config_entry, data): +async def webhook_render_template( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: """Handle a render template webhook.""" resp = {} for key, item in data.items(): try: tpl = template.Template(item[ATTR_TEMPLATE], hass) resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) - except template.TemplateError as ex: + except TemplateError as ex: resp[key] = {"error": str(ex)} return webhook_response(resp, registration=config_entry.data) @@ -351,7 +369,9 @@ async def webhook_render_template(hass, config_entry, data): }, ) ) -async def webhook_update_location(hass, config_entry, data): +async def webhook_update_location( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: """Handle an update location webhook.""" async_dispatcher_send( hass, SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data @@ -370,7 +390,9 @@ async def webhook_update_location(hass, config_entry, data): vol.Optional(ATTR_OS_VERSION): cv.string, } ) -async def webhook_update_registration(hass, config_entry, data): +async def webhook_update_registration( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: """Handle an update registration webhook.""" new_registration = {**config_entry.data, **data} @@ -396,7 +418,9 @@ async def webhook_update_registration(hass, config_entry, data): @WEBHOOK_COMMANDS.register("enable_encryption") -async def webhook_enable_encryption(hass, config_entry, data): +async def webhook_enable_encryption( + hass: HomeAssistant, config_entry: ConfigEntry, data: Any +) -> Response: """Handle a encryption enable webhook.""" if config_entry.data[ATTR_SUPPORTS_ENCRYPTION]: _LOGGER.warning( @@ -416,14 +440,18 @@ async def webhook_enable_encryption(hass, config_entry, data): secret = secrets.token_hex(SecretBox.KEY_SIZE) - data = {**config_entry.data, ATTR_SUPPORTS_ENCRYPTION: True, CONF_SECRET: secret} + update_data = { + **config_entry.data, + ATTR_SUPPORTS_ENCRYPTION: True, + CONF_SECRET: secret, + } - hass.config_entries.async_update_entry(config_entry, data=data) + hass.config_entries.async_update_entry(config_entry, data=update_data) return json_response({"secret": secret}) -def _validate_state_class_sensor(value: dict): +def _validate_state_class_sensor(value: dict[str, Any]) -> dict[str, Any]: """Validate we only set state class for sensors.""" if ( ATTR_SENSOR_STATE_CLASS in value @@ -434,12 +462,12 @@ def _validate_state_class_sensor(value: dict): return value -def _gen_unique_id(webhook_id, sensor_unique_id): +def _gen_unique_id(webhook_id: str, sensor_unique_id: str) -> str: """Return a unique sensor ID.""" return f"{webhook_id}_{sensor_unique_id}" -def _extract_sensor_unique_id(webhook_id, unique_id): +def _extract_sensor_unique_id(webhook_id: str, unique_id: str) -> str: """Return a unique sensor ID.""" return unique_id[len(webhook_id) + 1 :] @@ -467,11 +495,13 @@ def _extract_sensor_unique_id(webhook_id, unique_id): _validate_state_class_sensor, ) ) -async def webhook_register_sensor(hass, config_entry, data): +async def webhook_register_sensor( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: """Handle a register sensor webhook.""" - entity_type = data[ATTR_SENSOR_TYPE] - unique_id = data[ATTR_SENSOR_UNIQUE_ID] - device_name = config_entry.data[ATTR_DEVICE_NAME] + entity_type: str = data[ATTR_SENSOR_TYPE] + unique_id: str = data[ATTR_SENSOR_UNIQUE_ID] + device_name: str = config_entry.data[ATTR_DEVICE_NAME] unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id) entity_registry = er.async_get(hass) @@ -488,7 +518,8 @@ async def webhook_register_sensor(hass, config_entry, data): ) entry = entity_registry.async_get(existing_sensor) - changes = {} + assert entry is not None + changes: dict[str, Any] = {} if ( new_name := f"{device_name} {data[ATTR_SENSOR_NAME]}" @@ -551,7 +582,9 @@ async def webhook_register_sensor(hass, config_entry, data): ], ) ) -async def webhook_update_sensor_states(hass, config_entry, data): +async def webhook_update_sensor_states( + hass: HomeAssistant, config_entry: ConfigEntry, data: list[dict[str, Any]] +) -> Response: """Handle an update sensor states webhook.""" sensor_schema_full = vol.Schema( { @@ -563,14 +596,14 @@ async def webhook_update_sensor_states(hass, config_entry, data): } ) - device_name = config_entry.data[ATTR_DEVICE_NAME] - resp = {} + device_name: str = config_entry.data[ATTR_DEVICE_NAME] + resp: dict[str, Any] = {} entity_registry = er.async_get(hass) for sensor in data: - entity_type = sensor[ATTR_SENSOR_TYPE] + entity_type: str = sensor[ATTR_SENSOR_TYPE] - unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] + unique_id: str = sensor[ATTR_SENSOR_UNIQUE_ID] unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id) @@ -620,14 +653,16 @@ async def webhook_update_sensor_states(hass, config_entry, data): # Check if disabled entry = entity_registry.async_get(entity_id) - if entry.disabled_by: + if entry and entry.disabled_by: resp[unique_id]["is_disabled"] = True return webhook_response(resp, registration=config_entry.data) @WEBHOOK_COMMANDS.register("get_zones") -async def webhook_get_zones(hass, config_entry, data): +async def webhook_get_zones( + hass: HomeAssistant, config_entry: ConfigEntry, data: Any +) -> Response: """Handle a get zones webhook.""" zones = [ hass.states.get(entity_id) @@ -637,7 +672,9 @@ async def webhook_get_zones(hass, config_entry, data): @WEBHOOK_COMMANDS.register("get_config") -async def webhook_get_config(hass, config_entry, data): +async def webhook_get_config( + hass: HomeAssistant, config_entry: ConfigEntry, data: Any +) -> Response: """Handle a get config webhook.""" hass_config = hass.config.as_dict() @@ -656,8 +693,9 @@ async def webhook_get_config(hass, config_entry, data): if CONF_CLOUDHOOK_URL in config_entry.data: resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL] - with suppress(hass.components.cloud.CloudNotAvailable): - resp[CONF_REMOTE_UI_URL] = cloud.async_remote_ui_url(hass) + if cloud.async_active_subscription(hass): + with suppress(hass.components.cloud.CloudNotAvailable): + resp[CONF_REMOTE_UI_URL] = cloud.async_remote_ui_url(hass) webhook_id = config_entry.data[CONF_WEBHOOK_ID] @@ -679,12 +717,14 @@ async def webhook_get_config(hass, config_entry, data): @WEBHOOK_COMMANDS.register("scan_tag") @validate_schema({vol.Required("tag_id"): cv.string}) -async def webhook_scan_tag(hass, config_entry, data): +async def webhook_scan_tag( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, str] +) -> Response: """Handle a fire event webhook.""" await tag.async_scan_tag( hass, data["tag_id"], - config_entry.data[ATTR_DEVICE_ID], + hass.data[DOMAIN][DATA_DEVICES][config_entry.data[CONF_WEBHOOK_ID]].id, registration_context(config_entry.data), ) return empty_okay_response() diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index ec04d5f147d..a62d8537a33 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -9,7 +9,6 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, ) -from homeassistant.components.climate import HVACMode from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) @@ -66,6 +65,13 @@ from .const import ( # noqa: F401 CONF_DATA_TYPE, CONF_FANS, CONF_HUB, + CONF_HVAC_MODE_AUTO, + CONF_HVAC_MODE_COOL, + CONF_HVAC_MODE_DRY, + CONF_HVAC_MODE_FAN_ONLY, + CONF_HVAC_MODE_HEAT, + CONF_HVAC_MODE_HEAT_COOL, + CONF_HVAC_MODE_OFF, CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, CONF_HVAC_ONOFF_REGISTER, @@ -227,13 +233,13 @@ CLIMATE_SCHEMA = vol.All( { CONF_ADDRESS: cv.positive_int, CONF_HVAC_MODE_VALUES: { - vol.Optional(HVACMode.OFF.value): cv.positive_int, - vol.Optional(HVACMode.HEAT.value): cv.positive_int, - vol.Optional(HVACMode.COOL.value): cv.positive_int, - vol.Optional(HVACMode.HEAT_COOL.value): cv.positive_int, - vol.Optional(HVACMode.AUTO.value): cv.positive_int, - vol.Optional(HVACMode.DRY.value): cv.positive_int, - vol.Optional(HVACMode.FAN_ONLY.value): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_OFF): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_HEAT): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_COOL): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_HEAT_COOL): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_AUTO): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_DRY): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_FAN_ONLY): cv.positive_int, }, } ), @@ -287,7 +293,12 @@ BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In( - [CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING] + [ + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + ] ), vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int, } diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index ed2f6fa0227..06d7b1b6a11 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -60,7 +60,7 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): """Initialize the Modbus binary sensor.""" self._count = slave_count + 1 self._coordinator: DataUpdateCoordinator[Any] | None = None - self._result = None + self._result: list = [] super().__init__(hub, entry) async def async_setup_slaves( @@ -106,15 +106,15 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): return self._lazy_errors = self._lazy_error_count self._attr_available = False - self._result = None + self._result = [] else: self._lazy_errors = self._lazy_error_count self._attr_available = True - self._result = result if self._input_type in (CALL_TYPE_COIL, CALL_TYPE_DISCRETE): - self._attr_is_on = bool(result.bits[0] & 1) + self._result = result.bits else: - self._attr_is_on = bool(result.registers[0] & 1) + self._result = result.registers + self._attr_is_on = bool(self._result[0] & 1) self.async_write_ha_state() if self._coordinator: @@ -132,8 +132,7 @@ class SlaveSensor(CoordinatorEntity, RestoreEntity, BinarySensorEntity): self._attr_name = f"{entry[CONF_NAME]} {idx}" self._attr_device_class = entry.get(CONF_DEVICE_CLASS) self._attr_available = False - self._result_inx = int(idx / 8) - self._result_bit = 2 ** (idx % 8) + self._result_inx = idx super().__init__(coordinator) async def async_added_to_hass(self) -> None: @@ -148,5 +147,5 @@ class SlaveSensor(CoordinatorEntity, RestoreEntity, BinarySensorEntity): """Handle updated data from the coordinator.""" result = self.coordinator.data if result: - self._attr_is_on = result.bits[self._result_inx] & self._result_bit + self._attr_is_on = bool(result[self._result_inx] & 1) super()._handle_coordinator_update() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 92efcfb17d5..4e6fdf6cae7 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -32,6 +32,13 @@ from .const import ( CALL_TYPE_WRITE_REGISTER, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, + CONF_HVAC_MODE_AUTO, + CONF_HVAC_MODE_COOL, + CONF_HVAC_MODE_DRY, + CONF_HVAC_MODE_FAN_ONLY, + CONF_HVAC_MODE_HEAT, + CONF_HVAC_MODE_HEAT_COOL, + CONF_HVAC_MODE_OFF, CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, CONF_HVAC_ONOFF_REGISTER, @@ -99,10 +106,19 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._attr_hvac_mode = None self._hvac_mode_mapping: list[tuple[int, HVACMode]] = [] mode_value_config = mode_config[CONF_HVAC_MODE_VALUES] - for hvac_mode in HVACMode: - if hvac_mode.value in mode_value_config: + + for hvac_mode_kw, hvac_mode in ( + (CONF_HVAC_MODE_OFF, HVACMode.OFF), + (CONF_HVAC_MODE_HEAT, HVACMode.HEAT), + (CONF_HVAC_MODE_COOL, HVACMode.COOL), + (CONF_HVAC_MODE_HEAT_COOL, HVACMode.HEAT_COOL), + (CONF_HVAC_MODE_AUTO, HVACMode.AUTO), + (CONF_HVAC_MODE_DRY, HVACMode.DRY), + (CONF_HVAC_MODE_FAN_ONLY, HVACMode.FAN_ONLY), + ): + if hvac_mode_kw in mode_value_config: self._hvac_mode_mapping.append( - (mode_value_config[hvac_mode.value], hvac_mode) + (mode_value_config[hvac_mode_kw], hvac_mode) ) self._attr_hvac_modes.append(hvac_mode) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 2ad36f908ce..6ed52ae0544 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -56,6 +56,13 @@ CONF_TARGET_TEMP = "target_temp_register" CONF_HVAC_MODE_REGISTER = "hvac_mode_register" CONF_HVAC_MODE_VALUES = "values" CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register" +CONF_HVAC_MODE_OFF = "state_off" +CONF_HVAC_MODE_HEAT = "state_heat" +CONF_HVAC_MODE_COOL = "state_cool" +CONF_HVAC_MODE_HEAT_COOL = "state_heat_cool" +CONF_HVAC_MODE_AUTO = "state_auto" +CONF_HVAC_MODE_DRY = "state_dry" +CONF_HVAC_MODE_FAN_ONLY = "state_fan_only" CONF_VERIFY = "verify" CONF_VERIFY_REGISTER = "verify_register" CONF_VERIFY_STATE = "verify_state" diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index 024759791f4..58078a4ddb0 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -8,5 +8,6 @@ "dependencies": ["usb"], "iot_class": "local_polling", "usb": [{ "vid": "0572", "pid": "1340" }], - "loggers": ["phone_modem"] + "loggers": ["phone_modem"], + "integration_type": "device" } diff --git a/homeassistant/components/modem_callerid/translations/sk.json b/homeassistant/components/modem_callerid/translations/sk.json index f7ef4cd289d..a11a0c0cc82 100644 --- a/homeassistant/components/modem_callerid/translations/sk.json +++ b/homeassistant/components/modem_callerid/translations/sk.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "Nena\u0161li sa \u017eiadne zost\u00e1vaj\u00face zariadenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" }, "step": { "user": { diff --git a/homeassistant/components/modern_forms/translations/sk.json b/homeassistant/components/modern_forms/translations/sk.json new file mode 100644 index 00000000000..ed248c10965 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/sk.json b/homeassistant/components/moehlenhoff_alpha2/translations/sk.json new file mode 100644 index 00000000000..49599b94909 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index 85910b0eb9a..338c976534e 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -2,7 +2,7 @@ "domain": "monoprice", "name": "Monoprice 6-Zone Amplifier", "documentation": "https://www.home-assistant.io/integrations/monoprice", - "requirements": ["pymonoprice==0.3"], + "requirements": ["pymonoprice==0.4"], "codeowners": ["@etsinko", "@OnFreund"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/monoprice/translations/sk.json b/homeassistant/components/monoprice/translations/sk.json index 892b8b2cd91..1626328f220 100644 --- a/homeassistant/components/monoprice/translations/sk.json +++ b/homeassistant/components/monoprice/translations/sk.json @@ -1,10 +1,39 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { - "port": "Port" - } + "port": "Port", + "source_1": "N\u00e1zov zdroja #1", + "source_2": "N\u00e1zov zdroja #2", + "source_3": "N\u00e1zov zdroja #3", + "source_4": "N\u00e1zov zdroja #4", + "source_5": "N\u00e1zov zdroja #5", + "source_6": "N\u00e1zov zdroja #6" + }, + "title": "Pripojte sa k zariadeniu" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "N\u00e1zov zdroja #1", + "source_2": "N\u00e1zov zdroja #2", + "source_3": "N\u00e1zov zdroja #3", + "source_4": "N\u00e1zov zdroja #4", + "source_5": "N\u00e1zov zdroja #5", + "source_6": "N\u00e1zov zdroja #6" + }, + "title": "Konfigur\u00e1cia zdrojov" } } } diff --git a/homeassistant/components/moon/translations/sensor.sk.json b/homeassistant/components/moon/translations/sensor.sk.json new file mode 100644 index 00000000000..4adfde603cf --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.sk.json @@ -0,0 +1,10 @@ +{ + "state": { + "moon__phase": { + "first_quarter": "Prv\u00e1 \u0161tvrtina", + "full_moon": "Spln", + "last_quarter": "Posledn\u00e1 \u0161tvrtina", + "new_moon": "Nov mesiac" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sk.json b/homeassistant/components/moon/translations/sk.json new file mode 100644 index 00000000000..6ba11236f08 --- /dev/null +++ b/homeassistant/components/moon/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index a73166912f4..e0d02750d6d 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -380,7 +380,7 @@ class MotionTiltOnlyDevice(MotionTiltDevice): _restore_tilt = False @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" supported_features = ( CoverEntityFeature.OPEN_TILT diff --git a/homeassistant/components/motion_blinds/translations/sk.json b/homeassistant/components/motion_blinds/translations/sk.json index 6ea9c064f16..baae7e432f9 100644 --- a/homeassistant/components/motion_blinds/translations/sk.json +++ b/homeassistant/components/motion_blinds/translations/sk.json @@ -1,13 +1,26 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "connection_error": "Nepodarilo sa pripoji\u0165" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { "api_key": "API k\u013e\u00fa\u010d" } + }, + "select": { + "data": { + "select_ip": "IP adresa" + } + }, + "user": { + "data": { + "host": "IP adresa" + } } } } diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json index f5716dcf951..670f394fe6c 100644 --- a/homeassistant/components/motioneye/translations/bg.json +++ b/homeassistant/components/motioneye/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -13,7 +13,10 @@ "step": { "user": { "data": { - "admin_password": "\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0441\u043a\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", + "admin_password": "\u041f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "admin_username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "surveillance_password": "\u041f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435", + "surveillance_username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0437\u0430 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435", "url": "URL" } } diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json index e4d72b07398..cb60930bb45 100644 --- a/homeassistant/components/motioneye/translations/de.json +++ b/homeassistant/components/motioneye/translations/de.json @@ -31,7 +31,7 @@ "init": { "data": { "stream_url_template": "Stream-URL-Vorlage", - "webhook_set": "MotionEye-Webhooks konfigurieren, um Ereignisse an Home Assistant zu melden", + "webhook_set": "MotionEye Webhooks konfigurieren, um Ereignisse an Home Assistant zu melden", "webhook_set_overwrite": "\u00dcberschreiben von nicht bekannten Webhooks" } } diff --git a/homeassistant/components/motioneye/translations/sk.json b/homeassistant/components/motioneye/translations/sk.json index 71a7aea5018..f3eb264871f 100644 --- a/homeassistant/components/motioneye/translations/sk.json +++ b/homeassistant/components/motioneye/translations/sk.json @@ -1,10 +1,24 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_url": "Neplatn\u00e1 adresa URL", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "admin_password": "Spr\u00e1vca Heslo", + "admin_username": "Admin Pou\u017e\u00edvate\u013esk\u00e9 meno", + "surveillance_password": "Doh\u013ead Heslo", + "url": "URL" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 44d94bc649f..fd783e0975b 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -338,10 +338,10 @@ class MpdDevice(MediaPlayerEntity): return None @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self._status is None: - return 0 + return MediaPlayerEntityFeature(0) supported = SUPPORT_MPD if "volume" in self._status: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 06921105aae..1c0df640c08 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -76,7 +76,10 @@ from .const import ( # noqa: F401 CONF_TLS_INSECURE, CONF_TLS_VERSION, CONF_TOPIC, + CONF_TRANSPORT, CONF_WILL_MESSAGE, + CONF_WS_HEADERS, + CONF_WS_PATH, DATA_MQTT, DEFAULT_ENCODING, DEFAULT_QOS, @@ -95,12 +98,12 @@ from .models import ( # noqa: F401 ReceivePayloadType, ) from .util import ( - _VALID_QOS_SCHEMA, async_create_certificate_temp_files, get_mqtt_data, migrate_certificate_file_to_content, mqtt_config_entry_enabled, valid_publish_topic, + valid_qos_schema, valid_subscribe_topic, ) @@ -134,6 +137,9 @@ CONFIG_ENTRY_CONFIG_KEYS = [ CONF_PORT, CONF_PROTOCOL, CONF_TLS_INSECURE, + CONF_TRANSPORT, + CONF_WS_PATH, + CONF_WS_HEADERS, CONF_USERNAME, CONF_WILL_MESSAGE, ] @@ -172,7 +178,7 @@ MQTT_PUBLISH_SCHEMA = vol.All( vol.Exclusive(ATTR_TOPIC_TEMPLATE, CONF_TOPIC): cv.string, vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True, @@ -192,7 +198,7 @@ async def _async_setup_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Start the MQTT protocol service.""" + """Set up the MQTT protocol service.""" mqtt_data = get_mqtt_data(hass, True) conf: ConfigType | None = config.get(DOMAIN) @@ -464,9 +470,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _reload_config(call: ServiceCall) -> None: """Reload the platforms.""" - # Reload the legacy yaml platform - await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) - # Reload the modern yaml platforms mqtt_platforms = async_get_platforms(hass, DOMAIN) tasks = [ diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 00f6d357553..913a6e13400 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -130,16 +130,12 @@ ABBREVIATIONS = { "pl_cln_sp": "payload_clean_spot", "pl_cls": "payload_close", "pl_disarm": "payload_disarm", - "pl_hi_spd": "payload_high_speed", "pl_home": "payload_home", "pl_lock": "payload_lock", "pl_loc": "payload_locate", - "pl_lo_spd": "payload_low_speed", - "pl_med_spd": "payload_medium_speed", "pl_not_avail": "payload_not_available", "pl_not_home": "payload_not_home", "pl_off": "payload_off", - "pl_off_spd": "payload_off_speed", "pl_on": "payload_on", "pl_open": "payload_open", "pl_osc_off": "payload_oscillation_off", @@ -169,6 +165,7 @@ ABBREVIATIONS = { "pr_mode_stat_t": "preset_mode_state_topic", "pr_mode_val_tpl": "preset_mode_value_template", "pr_modes": "preset_modes", + "ptrn": "pattern", "r_tpl": "red_template", "rel_s": "release_summary", "rel_u": "release_url", @@ -192,12 +189,8 @@ ABBREVIATIONS = { "set_pos_t": "set_position_topic", "pos_t": "position_topic", "pos_tpl": "position_template", - "spd_cmd_t": "speed_command_topic", - "spd_stat_t": "speed_state_topic", "spd_rng_min": "speed_range_min", "spd_rng_max": "speed_range_max", - "spd_val_tpl": "speed_value_template", - "spds": "speeds", "src_type": "source_type", "stat_cla": "state_class", "stat_clsd": "state_closed", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index ed1990d919e..aa796d9ea8f 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -45,10 +45,9 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -113,32 +112,15 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT alarm control panels under the alarm_control_panel platform key is deprecated in HA Core 2022.6 +# Configuring MQTT alarm control panels under the alarm_control_panel platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(alarm.DOMAIN), ) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT alarm control panel configured under the alarm_control_panel key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - alarm.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -155,8 +137,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) @@ -168,32 +150,39 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): _entity_id_format = alarm.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Init the MQTT Alarm Control Panel.""" self._state: str | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" self._value_template = MqttValueTemplate( - self._config.get(CONF_VALUE_TEMPLATE), + config.get(CONF_VALUE_TEMPLATE), entity=self, ).async_render_with_possible_json_value self._command_template = MqttCommandTemplate( - self._config[CONF_COMMAND_TEMPLATE], entity=self + config[CONF_COMMAND_TEMPLATE], entity=self ).async_render - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Run when new MQTT message has been received.""" payload = self._value_template(msg.payload) if payload not in ( @@ -210,7 +199,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): ): _LOGGER.warning("Received unexpected payload: %s", msg.payload) return - self._state = payload + self._state = str(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( @@ -226,7 +215,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -236,7 +225,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): return self._state @property - def supported_features(self) -> int: + def supported_features(self) -> AlarmControlPanelEntityFeature: """Return the list of supported features.""" return ( AlarmControlPanelEntityFeature.ARM_HOME @@ -250,6 +239,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): @property def code_format(self) -> alarm.CodeFormat | None: """Return one or more digits/characters.""" + code: str | None if (code := self._config.get(CONF_CODE)) is None: return None if code == REMOTE_CODE or (isinstance(code, str) and re.search("^\\d+$", code)): @@ -259,17 +249,17 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): @property def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" - return self._config[CONF_CODE_ARM_REQUIRED] + return bool(self._config[CONF_CODE_ARM_REQUIRED]) async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command. This method is a coroutine. """ - code_required = self._config[CONF_CODE_DISARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_DISARM_REQUIRED] if code_required and not self._validate_code(code, "disarming"): return - payload = self._config[CONF_PAYLOAD_DISARM] + payload: str = self._config[CONF_PAYLOAD_DISARM] await self._publish(code, payload) async def async_alarm_arm_home(self, code: str | None = None) -> None: @@ -277,10 +267,10 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming home"): return - action = self._config[CONF_PAYLOAD_ARM_HOME] + action: str = self._config[CONF_PAYLOAD_ARM_HOME] await self._publish(code, action) async def async_alarm_arm_away(self, code: str | None = None) -> None: @@ -288,10 +278,10 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming away"): return - action = self._config[CONF_PAYLOAD_ARM_AWAY] + action: str = self._config[CONF_PAYLOAD_ARM_AWAY] await self._publish(code, action) async def async_alarm_arm_night(self, code: str | None = None) -> None: @@ -299,10 +289,10 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming night"): return - action = self._config[CONF_PAYLOAD_ARM_NIGHT] + action: str = self._config[CONF_PAYLOAD_ARM_NIGHT] await self._publish(code, action) async def async_alarm_arm_vacation(self, code: str | None = None) -> None: @@ -310,10 +300,10 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming vacation"): return - action = self._config[CONF_PAYLOAD_ARM_VACATION] + action: str = self._config[CONF_PAYLOAD_ARM_VACATION] await self._publish(code, action) async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: @@ -321,10 +311,10 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming custom bypass"): return - action = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS] + action: str = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS] await self._publish(code, action) async def async_alarm_trigger(self, code: str | None = None) -> None: @@ -332,13 +322,13 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): This method is a coroutine. """ - code_required = self._config[CONF_CODE_TRIGGER_REQUIRED] + code_required: bool = self._config[CONF_CODE_TRIGGER_REQUIRED] if code_required and not self._validate_code(code, "triggering"): return - action = self._config[CONF_PAYLOAD_TRIGGER] + action: str = self._config[CONF_PAYLOAD_TRIGGER] await self._publish(code, action) - async def _publish(self, code, action): + async def _publish(self, code: str | None, action: str) -> None: """Publish via mqtt.""" variables = {"action": action, "code": code} payload = self._command_template(None, variables=variables) @@ -350,10 +340,10 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): self._config[CONF_ENCODING], ) - def _validate_code(self, code, state): + def _validate_code(self, code: str | None, state: str) -> bool: """Validate given code.""" - conf_code = self._config.get(CONF_CODE) - check = ( + conf_code: str | None = self._config.get(CONF_CODE) + check = bool( conf_code is None or code == conf_code or (conf_code == REMOTE_CODE and code) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 915a2780283..bbbcb97ada6 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,16 +1,16 @@ """Support for MQTT binary sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import functools import logging +from typing import Any import voluptuous as vol from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -25,7 +25,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.event as evt @@ -43,10 +43,9 @@ from .mixins import ( MqttAvailability, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -70,32 +69,15 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Binary sensors under the binary_sensor platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Binary sensors under the binary_sensor platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(binary_sensor.DOMAIN), ) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT binary sensor configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - binary_sensor.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -112,8 +94,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT binary sensor.""" async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) @@ -123,17 +105,18 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Representation a binary sensor that is updated by MQTT.""" _entity_id_format = binary_sensor.ENTITY_ID_FORMAT + _expired: bool | None - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT binary sensor.""" - self._state: bool | None = None - self._expiration_trigger = None - self._delay_listener = None - expire_after = config.get(CONF_EXPIRE_AFTER) - if expire_after is not None and expire_after > 0: - self._expired = True - else: - self._expired = None + self._expiration_trigger: CALLBACK_TYPE | None = None + self._delay_listener: CALLBACK_TYPE | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -148,13 +131,15 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): # MqttEntity.async_added_to_hass(), then we should not restore state and not self._expiration_trigger ): - expiration_at = last_state.last_changed + timedelta(seconds=expire_after) + expiration_at: datetime = last_state.last_changed + timedelta( + seconds=expire_after + ) if expiration_at < (time_now := dt_util.utcnow()): # Skip reactivating the binary_sensor _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) return self._expired = False - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at @@ -176,33 +161,41 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): await MqttEntity.async_will_remove_from_hass(self) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + expire_after: int | None = config.get(CONF_EXPIRE_AFTER) + if expire_after is not None and expire_after > 0: + self._expired = True + else: + self._expired = None self._attr_force_update = config[CONF_FORCE_UPDATE] + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._value_template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback - def off_delay_listener(now): + def off_delay_listener(now: datetime) -> None: """Switch device off after a delay.""" self._delay_listener = None - self._state = False + self._attr_is_on = False self.async_write_ha_state() @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle a new received MQTT state message.""" # auto-expire enabled? - expire_after = self._config.get(CONF_EXPIRE_AFTER) + expire_after: int | None = self._config.get(CONF_EXPIRE_AFTER) if expire_after is not None and expire_after > 0: @@ -233,15 +226,15 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): return if payload == self._config[CONF_PAYLOAD_ON]: - self._state = True + self._attr_is_on = True elif payload == self._config[CONF_PAYLOAD_OFF]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None else: # Payload is not for this entity template_info = "" if self._config.get(CONF_VALUE_TEMPLATE) is not None: - template_info = f", template output: '{payload}', with value template '{str(self._config.get(CONF_VALUE_TEMPLATE))}'" + template_info = f", template output: '{str(payload)}', with value template '{str(self._config.get(CONF_VALUE_TEMPLATE))}'" _LOGGER.info( "No matching payload found for entity: %s with state topic: %s. Payload: '%s'%s", self._config[CONF_NAME], @@ -256,7 +249,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): self._delay_listener = None off_delay = self._config.get(CONF_OFF_DELAY) - if self._state and off_delay is not None: + if self._attr_is_on and off_delay is not None: self._delay_listener = evt.async_call_later( self.hass, off_delay, off_delay_listener ) @@ -276,32 +269,22 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @callback - def _value_is_expired(self, *_): + def _value_is_expired(self, *_: Any) -> None: """Triggered when value is expired.""" self._expiration_trigger = None self._expired = True self.async_write_ha_state() - @property - def is_on(self) -> bool | None: - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self) -> BinarySensorDeviceClass | None: - """Return the class of this sensor.""" - return self._config.get(CONF_DEVICE_CLASS) - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" - expire_after = self._config.get(CONF_EXPIRE_AFTER) + expire_after: int | None = self._config.get(CONF_EXPIRE_AFTER) # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 return MqttAvailability.available.fget(self) and ( # type: ignore[attr-defined] expire_after is None or not self._expired diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index a14bf87c3be..929e270f300 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -26,7 +26,6 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) from .models import MqttCommandTemplate @@ -47,9 +46,9 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Buttons under the button platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Buttons under the button platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(button.DOMAIN), ) @@ -57,23 +56,6 @@ PLATFORM_SCHEMA = vol.All( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT button configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - button.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -90,8 +72,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT button.""" async_add_entities([MqttButton(hass, config, config_entry, discovery_data)]) @@ -102,25 +84,31 @@ class MqttButton(MqttEntity, ButtonEntity): _entity_id_format = button.ENTITY_ID_FORMAT - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT button.""" MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._command_template = MqttCommandTemplate( config.get(CONF_COMMAND_TEMPLATE), entity=self ).async_render - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @property diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index f6039251882..6ece232775a 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -18,15 +18,15 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING +from .const import CONF_QOS, CONF_TOPIC from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import ReceiveMessage from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -44,20 +44,6 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( } ) - -# Using CONF_ENCODING to set b64 encoding for images is deprecated as of Home Assistant 2022.9 -# use CONF_IMAGE_ENCODING instead, support for the work-a-round will be removed with Home Assistant 2022.11 -def repair_legacy_encoding(config: ConfigType) -> ConfigType: - """Check incorrect deprecated config of image encoding.""" - if config[CONF_ENCODING] == "b64": - config[CONF_IMAGE_ENCODING] = "b64" - config[CONF_ENCODING] = DEFAULT_ENCODING - _LOGGER.warning( - "Using the `encoding` parameter to set image encoding has been deprecated, use `image_encoding` instead" - ) - return config - - PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -68,36 +54,17 @@ PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( PLATFORM_SCHEMA_MODERN = vol.All( PLATFORM_SCHEMA_BASE.schema, - repair_legacy_encoding, ) -# Configuring MQTT Camera under the camera platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Camera under the camera platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_BASE.schema), warn_for_legacy_schema(camera.DOMAIN), - repair_legacy_encoding, ) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT camera configured under the camera platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - camera.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -114,8 +81,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Camera.""" async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)]) @@ -124,31 +91,38 @@ async def _async_setup_entity( class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" - _entity_id_format = camera.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_CAMERA_ATTRIBUTES_BLOCKED + _entity_id_format: str = camera.ENTITY_ID_FORMAT + _attributes_extra_blocked: frozenset[str] = MQTT_CAMERA_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT Camera.""" - self._last_image = None + self._last_image: bytes | None = None Camera.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" if CONF_IMAGE_ENCODING in self._config: self._last_image = b64decode(msg.payload) else: + assert isinstance(msg.payload, bytes) self._last_image = msg.payload self._sub_state = subscription.async_prepare_subscribe_topics( @@ -164,7 +138,7 @@ class MqttCamera(MqttEntity, Camera): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index e909a378581..36e12c47419 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -15,7 +15,6 @@ import uuid import attr import certifi -from paho.mqtt.client import MQTTMessage from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -52,13 +51,19 @@ from .const import ( CONF_CLIENT_KEY, CONF_KEEPALIVE, CONF_TLS_INSECURE, + CONF_TRANSPORT, CONF_WILL_MESSAGE, + CONF_WS_HEADERS, + CONF_WS_PATH, DEFAULT_ENCODING, DEFAULT_PROTOCOL, DEFAULT_QOS, + DEFAULT_TRANSPORT, MQTT_CONNECTED, MQTT_DISCONNECTED, + PROTOCOL_5, PROTOCOL_31, + TRANSPORT_WEBSOCKETS, ) from .models import ( AsyncMessageCallbackType, @@ -75,7 +80,6 @@ if TYPE_CHECKING: # because integrations should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt - _LOGGER = logging.getLogger(__name__) DISCOVERY_COOLDOWN = 2 @@ -143,16 +147,20 @@ AsyncDeprecatedMessageCallbackType = Callable[ [str, ReceivePayloadType, int], Coroutine[Any, Any, None] ] DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] +DeprecatedMessageCallbackTypes = Union[ + AsyncDeprecatedMessageCallbackType, DeprecatedMessageCallbackType +] +# Support for a deprecated callback type will be removed from HA core 2023.2.0 def wrap_msg_callback( - msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, + msg_callback: DeprecatedMessageCallbackTypes, ) -> AsyncMessageCallbackType | MessageCallbackType: """Wrap an MQTT message callback to support deprecated signature.""" # Check for partials to properly determine if coroutine function check_func = msg_callback while isinstance(check_func, partial): - check_func = check_func.func + check_func = check_func.func # type: ignore[unreachable] wrapper_func: AsyncMessageCallbackType | MessageCallbackType if asyncio.iscoroutinefunction(check_func): @@ -165,14 +173,15 @@ def wrap_msg_callback( ) wrapper_func = async_wrapper - else: + return wrapper_func - @wraps(msg_callback) - def wrapper(msg: ReceiveMessage) -> None: - """Call with deprecated signature.""" - msg_callback(msg.topic, msg.payload, msg.qos) + @wraps(msg_callback) + def wrapper(msg: ReceiveMessage) -> None: + """Call with deprecated signature.""" + msg_callback(msg.topic, msg.payload, msg.qos) + + wrapper_func = wrapper - wrapper_func = wrapper return wrapper_func @@ -182,8 +191,7 @@ async def async_subscribe( topic: str, msg_callback: AsyncMessageCallbackType | MessageCallbackType - | DeprecatedMessageCallbackType - | AsyncDeprecatedMessageCallbackType, + | DeprecatedMessageCallbackTypes, qos: int = DEFAULT_QOS, encoding: str | None = DEFAULT_ENCODING, ) -> CALLBACK_TYPE: @@ -196,6 +204,7 @@ async def async_subscribe( raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled" ) + # Support for a deprecated callback type will be removed from HA core 2023.2.0 # Count callback parameters which don't have a default value non_default = 0 if msg_callback: @@ -209,12 +218,13 @@ async def async_subscribe( if non_default == 3: module = inspect.getmodule(msg_callback) _LOGGER.warning( - "Signature of MQTT msg_callback '%s.%s' is deprecated", + "Signature of MQTT msg_callback '%s.%s' is deprecated, " + "this will stop working with HA core 2023.2", module.__name__ if module else "", msg_callback.__name__, ) wrapped_msg_callback = wrap_msg_callback( - cast(DeprecatedMessageCallbackType, msg_callback) + cast(DeprecatedMessageCallbackTypes, msg_callback) ) async_remove = await mqtt_data.client.async_subscribe( @@ -273,8 +283,10 @@ class MqttClientSetup: # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - if config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL) == PROTOCOL_31: + if (protocol := config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL)) == PROTOCOL_31: proto = mqtt.MQTTv31 + elif protocol == PROTOCOL_5: + proto = mqtt.MQTTv5 else: proto = mqtt.MQTTv311 @@ -282,7 +294,8 @@ class MqttClientSetup: # PAHO MQTT relies on the MQTT server to generate random client IDs. # However, that feature is not mandatory so we generate our own. client_id = mqtt.base62(uuid.uuid4().int, padding=22) - self._client = mqtt.Client(client_id, protocol=proto) + transport = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT) + self._client = mqtt.Client(client_id, protocol=proto, transport=transport) # Enable logging self._client.enable_logger() @@ -300,6 +313,10 @@ class MqttClientSetup: client_key = get_file_path(CONF_CLIENT_KEY, config.get(CONF_CLIENT_KEY)) client_cert = get_file_path(CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT)) tls_insecure = config.get(CONF_TLS_INSECURE) + if transport == TRANSPORT_WEBSOCKETS: + ws_path = config.get(CONF_WS_PATH) + ws_headers = config.get(CONF_WS_HEADERS) + self._client.ws_set_options(ws_path, ws_headers) if certificate is not None: self._client.tls_set( certificate, @@ -559,8 +576,9 @@ class MQTT: self, _mqttc: mqtt.Client, _userdata: None, - _flags: dict[str, Any], + _flags: dict[str, int], result_code: int, + properties: mqtt.Properties | None = None, ) -> None: """On connect callback. @@ -620,7 +638,7 @@ class MQTT: ) def _mqtt_on_message( - self, _mqttc: mqtt.Client, _userdata: None, msg: MQTTMessage + self, _mqttc: mqtt.Client, _userdata: None, msg: mqtt.MQTTMessage ) -> None: """Message received callback.""" self.hass.add_job(self._mqtt_handle_message, msg) @@ -634,7 +652,7 @@ class MQTT: return subscriptions @callback - def _mqtt_handle_message(self, msg: MQTTMessage) -> None: + def _mqtt_handle_message(self, msg: mqtt.MQTTMessage) -> None: _LOGGER.debug( "Received%s message on %s: %s", " retained" if msg.retain else "", @@ -678,9 +696,13 @@ class MQTT: _mqttc: mqtt.Client, _userdata: None, mid: int, - _granted_qos: tuple[Any, ...] | None = None, + _granted_qos_reason: tuple[int, ...] | mqtt.ReasonCodes | None = None, + _properties_reason: mqtt.ReasonCodes | None = None, ) -> None: """Publish / Subscribe / Unsubscribe callback.""" + # The callback signature for on_unsubscribe is different from on_subscribe + # see https://github.com/eclipse/paho.mqtt.python/issues/687 + # properties and reasoncodes are not used in Home Assistant self.hass.add_job(self._mqtt_handle_mid, mid) async def _mqtt_handle_mid(self, mid: int) -> None: @@ -696,7 +718,11 @@ class MQTT: self._pending_operations[mid] = asyncio.Event() def _mqtt_on_disconnect( - self, _mqttc: mqtt.Client, _userdata: None, result_code: int + self, + _mqttc: mqtt.Client, + _userdata: None, + result_code: int, + properties: mqtt.Properties | None = None, ) -> None: """Disconnected callback.""" self.connected = False diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 9f98fcfebdc..7d6d2ff7b2c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,6 +1,7 @@ """Support for MQTT climate devices.""" from __future__ import annotations +from collections.abc import Callable import functools import logging from typing import Any @@ -41,6 +42,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -51,10 +53,15 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -197,9 +204,9 @@ TOPIC_KEYS = ( ) -def valid_preset_mode_configuration(config): +def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the preset mode reset payload is not one of the preset modes.""" - if PRESET_NONE in config.get(CONF_PRESET_MODES_LIST): + if PRESET_NONE in config[CONF_PRESET_MODES_LIST]: raise ValueError("preset_modes must not include preset mode 'none'") return config @@ -300,10 +307,9 @@ PLATFORM_SCHEMA_MODERN = vol.All( valid_preset_mode_configuration, ) -# Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Climate under the climate platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - valid_preset_mode_configuration, warn_for_legacy_schema(climate.DOMAIN), ) @@ -326,23 +332,6 @@ DISCOVERY_SCHEMA = vol.All( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT climate configured under the fan platform key (deprecated).""" - # The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - climate.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -359,8 +348,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT climate devices.""" async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) @@ -372,65 +361,79 @@ class MqttClimate(MqttEntity, ClimateEntity): _entity_id_format = climate.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): - """Initialize the climate device.""" - self._action = None - self._aux = False - self._current_fan_mode = None - self._current_operation = None - self._current_swing_mode = None - self._current_temp = None - self._preset_mode = None - self._target_temp = None - self._target_temp_high = None - self._target_temp_low = None - self._topic = None - self._value_templates = None - self._command_templates = None - self._feature_preset_mode = False - self._optimistic_preset_mode = None + _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] + _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _feature_preset_mode: bool + _optimistic_preset_mode: bool + _topic: dict[str, Any] + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the climate device.""" MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_hvac_modes = config[CONF_MODE_LIST] + self._attr_min_temp = config[CONF_TEMP_MIN] + self._attr_max_temp = config[CONF_TEMP_MAX] + self._attr_precision = config.get(CONF_PRECISION, super().precision) + self._attr_fan_modes = config[CONF_FAN_MODE_LIST] + self._attr_swing_modes = config[CONF_SWING_MODE_LIST] + self._attr_target_temperature_step = config[CONF_TEMP_STEP] + self._attr_temperature_unit = config.get( + CONF_TEMPERATURE_UNIT, self.hass.config.units.temperature_unit + ) + self._topic = {key: config.get(key) for key in TOPIC_KEYS} # set to None in non-optimistic mode - self._target_temp = ( - self._current_fan_mode - ) = self._current_operation = self._current_swing_mode = None - self._target_temp_low = None - self._target_temp_high = None + self._attr_target_temperature = None + self._attr_fan_mode = None + self._attr_hvac_mode = None + self._attr_swing_mode = None + self._attr_target_temperature_low = None + self._attr_target_temperature_high = None if self._topic[CONF_TEMP_STATE_TOPIC] is None: - self._target_temp = config[CONF_TEMP_INITIAL] + self._attr_target_temperature = config[CONF_TEMP_INITIAL] if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None: - self._target_temp_low = config[CONF_TEMP_INITIAL] + self._attr_target_temperature_low = config[CONF_TEMP_INITIAL] if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None: - self._target_temp_high = config[CONF_TEMP_INITIAL] + self._attr_target_temperature_high = config[CONF_TEMP_INITIAL] if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: - self._current_fan_mode = FAN_LOW + self._attr_fan_mode = FAN_LOW if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: - self._current_swing_mode = SWING_OFF + self._attr_swing_mode = SWING_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None: - self._current_operation = HVACMode.OFF + self._attr_hvac_mode = HVACMode.OFF self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: - self._preset_modes = config[CONF_PRESET_MODES_LIST] + presets = [] + presets.extend(config[CONF_PRESET_MODES_LIST]) + if presets: + presets.insert(0, PRESET_NONE) + self._attr_preset_modes = presets + self._attr_preset_mode = PRESET_NONE else: - self._preset_modes = [] + self._attr_preset_modes = [] self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config - self._action = None - self._aux = False + self._attr_hvac_action = None - value_templates = {} + self._attr_is_aux_heat = False + + value_templates: dict[str, Template | None] = {} for key in VALUE_TEMPLATE_KEYS: value_templates[key] = None if CONF_VALUE_TEMPLATE in config: @@ -447,445 +450,13 @@ class MqttClimate(MqttEntity, ClimateEntity): for key, template in value_templates.items() } - command_templates = {} + self._command_templates = {} for key in COMMAND_TEMPLATE_KEYS: - command_templates[key] = MqttCommandTemplate( + self._command_templates[key] = MqttCommandTemplate( config.get(key), entity=self ).async_render - self._command_templates = command_templates - - def _prepare_subscribe_topics(self): # noqa: C901 - """(Re)Subscribe to topics.""" - topics = {} - qos = self._config[CONF_QOS] - - def add_subscription(topics, topic, msg_callback): - if self._topic[topic] is not None: - topics[topic] = { - "topic": self._topic[topic], - "msg_callback": msg_callback, - "qos": qos, - "encoding": self._config[CONF_ENCODING] or None, - } - - def render_template(msg, template_name): - template = self._value_templates[template_name] - return template(msg.payload) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_action_received(msg): - """Handle receiving action via MQTT.""" - payload = render_template(msg, CONF_ACTION_TEMPLATE) - if not payload or payload == PAYLOAD_NONE: - _LOGGER.debug( - "Invalid %s action: %s, ignoring", - [e.value for e in HVACAction], - payload, - ) - return - try: - self._action = HVACAction(payload) - except ValueError: - _LOGGER.warning( - "Invalid %s action: %s", - [e.value for e in HVACAction], - payload, - ) - return - get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - - add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) - - @callback - def handle_temperature_received(msg, template_name, attr): - """Handle temperature coming via MQTT.""" - payload = render_template(msg, template_name) - - try: - setattr(self, attr, float(payload)) - get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - except ValueError: - _LOGGER.error("Could not parse temperature from %s", payload) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_current_temperature_received(msg): - """Handle current temperature coming via MQTT.""" - handle_temperature_received( - msg, CONF_CURRENT_TEMP_TEMPLATE, "_current_temp" - ) - - add_subscription( - topics, CONF_CURRENT_TEMP_TOPIC, handle_current_temperature_received - ) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_target_temperature_received(msg): - """Handle target temperature coming via MQTT.""" - handle_temperature_received(msg, CONF_TEMP_STATE_TEMPLATE, "_target_temp") - - add_subscription( - topics, CONF_TEMP_STATE_TOPIC, handle_target_temperature_received - ) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_temperature_low_received(msg): - """Handle target temperature low coming via MQTT.""" - handle_temperature_received( - msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_target_temp_low" - ) - - add_subscription( - topics, CONF_TEMP_LOW_STATE_TOPIC, handle_temperature_low_received - ) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_temperature_high_received(msg): - """Handle target temperature high coming via MQTT.""" - handle_temperature_received( - msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_target_temp_high" - ) - - add_subscription( - topics, CONF_TEMP_HIGH_STATE_TOPIC, handle_temperature_high_received - ) - - @callback - def handle_mode_received(msg, template_name, attr, mode_list): - """Handle receiving listed mode via MQTT.""" - payload = render_template(msg, template_name) - - if payload not in self._config[mode_list]: - _LOGGER.error("Invalid %s mode: %s", mode_list, payload) - else: - setattr(self, attr, payload) - get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_current_mode_received(msg): - """Handle receiving mode via MQTT.""" - handle_mode_received( - msg, CONF_MODE_STATE_TEMPLATE, "_current_operation", CONF_MODE_LIST - ) - - add_subscription(topics, CONF_MODE_STATE_TOPIC, handle_current_mode_received) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_fan_mode_received(msg): - """Handle receiving fan mode via MQTT.""" - handle_mode_received( - msg, - CONF_FAN_MODE_STATE_TEMPLATE, - "_current_fan_mode", - CONF_FAN_MODE_LIST, - ) - - add_subscription(topics, CONF_FAN_MODE_STATE_TOPIC, handle_fan_mode_received) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_swing_mode_received(msg): - """Handle receiving swing mode via MQTT.""" - handle_mode_received( - msg, - CONF_SWING_MODE_STATE_TEMPLATE, - "_current_swing_mode", - CONF_SWING_MODE_LIST, - ) - - add_subscription( - topics, CONF_SWING_MODE_STATE_TOPIC, handle_swing_mode_received - ) - - @callback - def handle_onoff_mode_received(msg, template_name, attr): - """Handle receiving on/off mode via MQTT.""" - payload = render_template(msg, template_name) - payload_on = self._config[CONF_PAYLOAD_ON] - payload_off = self._config[CONF_PAYLOAD_OFF] - - if payload == "True": - payload = payload_on - elif payload == "False": - payload = payload_off - - if payload == payload_on: - setattr(self, attr, True) - elif payload == payload_off: - setattr(self, attr, False) - else: - _LOGGER.error("Invalid %s mode: %s", attr, payload) - - get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_aux_mode_received(msg): - """Handle receiving aux mode via MQTT.""" - handle_onoff_mode_received(msg, CONF_AUX_STATE_TEMPLATE, "_aux") - - add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) - - @callback - @log_messages(self.hass, self.entity_id) - def handle_preset_mode_received(msg): - """Handle receiving preset mode via MQTT.""" - preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) - if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: - self._preset_mode = None - get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - return - if not preset_mode: - _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) - return - if preset_mode not in self._preset_modes: - _LOGGER.warning( - "'%s' received on topic %s. '%s' is not a valid preset mode", - msg.payload, - msg.topic, - preset_mode, - ) - else: - self._preset_mode = preset_mode - get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - - add_subscription( - topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received - ) - - self._sub_state = subscription.async_prepare_subscribe_topics( - self.hass, self._sub_state, topics - ) - - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" - await subscription.async_subscribe_topics(self.hass, self._sub_state) - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - if unit := self._config.get(CONF_TEMPERATURE_UNIT): - return unit - return self.hass.config.units.temperature_unit - - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self._current_temp - - @property - def target_temperature(self) -> float | None: - """Return the temperature we try to reach.""" - return self._target_temp - - @property - def target_temperature_low(self) -> float | None: - """Return the low target temperature we try to reach.""" - return self._target_temp_low - - @property - def target_temperature_high(self) -> float | None: - """Return the high target temperature we try to reach.""" - return self._target_temp_high - - @property - def hvac_action(self) -> HVACAction | None: - """Return the current running hvac operation if supported.""" - return self._action - - @property - def hvac_mode(self) -> HVACMode: - """Return current operation ie. heat, cool, idle.""" - return self._current_operation - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available operation modes.""" - return self._config[CONF_MODE_LIST] - - @property - def target_temperature_step(self) -> float: - """Return the supported step of target temperature.""" - return self._config[CONF_TEMP_STEP] - - @property - def preset_mode(self) -> str | None: - """Return preset mode.""" - if self._feature_preset_mode and self._preset_mode is not None: - return self._preset_mode - return PRESET_NONE - - @property - def preset_modes(self) -> list[str]: - """Return preset modes.""" - presets = [] - presets.extend(self._preset_modes) - if presets: - presets.insert(0, PRESET_NONE) - - return presets - - @property - def is_aux_heat(self) -> bool | None: - """Return true if away mode is on.""" - return self._aux - - @property - def fan_mode(self) -> str | None: - """Return the fan setting.""" - return self._current_fan_mode - - @property - def fan_modes(self) -> list[str]: - """Return the list of available fan modes.""" - return self._config[CONF_FAN_MODE_LIST] - - async def _publish(self, topic, payload): - if self._topic[topic] is not None: - await self.async_publish( - self._topic[topic], - payload, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) - - async 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 - setattr(self, attr, temp) - - payload = self._command_templates[cmnd_template](temp) - await self._publish(cmnd_topic, payload) - - async def async_set_temperature(self, **kwargs: Any) -> None: - """Set new target temperatures.""" - if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: - await self.async_set_hvac_mode(operation_mode) - - await self._set_temperature( - kwargs.get(ATTR_TEMPERATURE), - CONF_TEMP_COMMAND_TOPIC, - CONF_TEMP_COMMAND_TEMPLATE, - CONF_TEMP_STATE_TOPIC, - "_target_temp", - ) - - await 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", - ) - - await 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", - ) - - self.async_write_ha_state() - - async def async_set_swing_mode(self, swing_mode: str) -> None: - """Set new swing mode.""" - payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode) - await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) - - if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: - self._current_swing_mode = swing_mode - self.async_write_ha_state() - - async def async_set_fan_mode(self, fan_mode: str) -> None: - """Set new target temperature.""" - payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) - await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) - - if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: - self._current_fan_mode = fan_mode - self.async_write_ha_state() - - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set new operation mode.""" - if hvac_mode == HVACMode.OFF: - await self._publish( - CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF] - ) - else: - await self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON]) - - payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode) - await self._publish(CONF_MODE_COMMAND_TOPIC, payload) - - if self._topic[CONF_MODE_STATE_TOPIC] is None: - self._current_operation = hvac_mode - self.async_write_ha_state() - - @property - def swing_mode(self) -> str | None: - """Return the swing setting.""" - return self._current_swing_mode - - @property - def swing_modes(self) -> list[str]: - """List of available swing modes.""" - return self._config[CONF_SWING_MODE_LIST] - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set a preset mode.""" - if self._feature_preset_mode: - if preset_mode not in self.preset_modes and preset_mode is not PRESET_NONE: - _LOGGER.warning("'%s' is not a valid preset mode", preset_mode) - return - mqtt_payload = self._command_templates[CONF_PRESET_MODE_COMMAND_TEMPLATE]( - preset_mode - ) - await self._publish( - CONF_PRESET_MODE_COMMAND_TOPIC, - mqtt_payload, - ) - - if self._optimistic_preset_mode: - self._preset_mode = preset_mode if preset_mode != PRESET_NONE else None - self.async_write_ha_state() - - return - - async def _set_aux_heat(self, state): - await self._publish( - CONF_AUX_COMMAND_TOPIC, - self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], - ) - - if self._topic[CONF_AUX_STATE_TOPIC] is None: - self._aux = state - self.async_write_ha_state() - - async def async_turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - await self._set_aux_heat(True) - - async def async_turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - await self._set_aux_heat(False) - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - support = 0 - + support = ClimateEntityFeature(0) if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or ( self._topic[CONF_TEMP_COMMAND_TOPIC] is not None ): @@ -918,22 +489,363 @@ class MqttClimate(MqttEntity, ClimateEntity): self._topic[CONF_AUX_COMMAND_TOPIC] is not None ): support |= ClimateEntityFeature.AUX_HEAT + self._attr_supported_features = support - return support + def _prepare_subscribe_topics(self) -> None: # noqa: C901 + """(Re)Subscribe to topics.""" + topics: dict[str, dict[str, Any]] = {} + qos: int = self._config[CONF_QOS] - @property - def min_temp(self) -> float: - """Return the minimum temperature.""" - return self._config[CONF_TEMP_MIN] + def add_subscription( + topics: dict[str, dict[str, Any]], + topic: str, + msg_callback: Callable[[ReceiveMessage], None], + ) -> None: + if self._topic[topic] is not None: + topics[topic] = { + "topic": self._topic[topic], + "msg_callback": msg_callback, + "qos": qos, + "encoding": self._config[CONF_ENCODING] or None, + } - @property - def max_temp(self) -> float: - """Return the maximum temperature.""" - return self._config[CONF_TEMP_MAX] + def render_template( + msg: ReceiveMessage, template_name: str + ) -> ReceivePayloadType: + template = self._value_templates[template_name] + return template(msg.payload) - @property - def precision(self) -> float: - """Return the precision of the system.""" - if (precision := self._config.get(CONF_PRECISION)) is not None: - return precision - return super().precision + @callback + @log_messages(self.hass, self.entity_id) + def handle_action_received(msg: ReceiveMessage) -> None: + """Handle receiving action via MQTT.""" + payload = render_template(msg, CONF_ACTION_TEMPLATE) + if not payload or payload == PAYLOAD_NONE: + _LOGGER.debug( + "Invalid %s action: %s, ignoring", + [e.value for e in HVACAction], + payload, + ) + return + try: + self._attr_hvac_action = HVACAction(str(payload)) + except ValueError: + _LOGGER.warning( + "Invalid %s action: %s", + [e.value for e in HVACAction], + payload, + ) + return + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) + + @callback + def handle_temperature_received( + msg: ReceiveMessage, template_name: str, attr: str + ) -> None: + """Handle temperature coming via MQTT.""" + payload = render_template(msg, template_name) + + try: + setattr(self, attr, float(payload)) + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + except ValueError: + _LOGGER.error("Could not parse temperature from %s", payload) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_current_temperature_received(msg: ReceiveMessage) -> None: + """Handle current temperature coming via MQTT.""" + handle_temperature_received( + msg, CONF_CURRENT_TEMP_TEMPLATE, "_attr_current_temperature" + ) + + add_subscription( + topics, CONF_CURRENT_TEMP_TOPIC, handle_current_temperature_received + ) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_target_temperature_received(msg: ReceiveMessage) -> None: + """Handle target temperature coming via MQTT.""" + handle_temperature_received( + msg, CONF_TEMP_STATE_TEMPLATE, "_attr_target_temperature" + ) + + add_subscription( + topics, CONF_TEMP_STATE_TOPIC, handle_target_temperature_received + ) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_temperature_low_received(msg: ReceiveMessage) -> None: + """Handle target temperature low coming via MQTT.""" + handle_temperature_received( + msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_attr_target_temperature_low" + ) + + add_subscription( + topics, CONF_TEMP_LOW_STATE_TOPIC, handle_temperature_low_received + ) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_temperature_high_received(msg: ReceiveMessage) -> None: + """Handle target temperature high coming via MQTT.""" + handle_temperature_received( + msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_attr_target_temperature_high" + ) + + add_subscription( + topics, CONF_TEMP_HIGH_STATE_TOPIC, handle_temperature_high_received + ) + + @callback + def handle_mode_received( + msg: ReceiveMessage, template_name: str, attr: str, mode_list: str + ) -> None: + """Handle receiving listed mode via MQTT.""" + payload = render_template(msg, template_name) + + if payload not in self._config[mode_list]: + _LOGGER.error("Invalid %s mode: %s", mode_list, payload) + else: + setattr(self, attr, payload) + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_current_mode_received(msg: ReceiveMessage) -> None: + """Handle receiving mode via MQTT.""" + handle_mode_received( + msg, CONF_MODE_STATE_TEMPLATE, "_attr_hvac_mode", CONF_MODE_LIST + ) + + add_subscription(topics, CONF_MODE_STATE_TOPIC, handle_current_mode_received) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_fan_mode_received(msg: ReceiveMessage) -> None: + """Handle receiving fan mode via MQTT.""" + handle_mode_received( + msg, + CONF_FAN_MODE_STATE_TEMPLATE, + "_attr_fan_mode", + CONF_FAN_MODE_LIST, + ) + + add_subscription(topics, CONF_FAN_MODE_STATE_TOPIC, handle_fan_mode_received) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_swing_mode_received(msg: ReceiveMessage) -> None: + """Handle receiving swing mode via MQTT.""" + handle_mode_received( + msg, + CONF_SWING_MODE_STATE_TEMPLATE, + "_attr_swing_mode", + CONF_SWING_MODE_LIST, + ) + + add_subscription( + topics, CONF_SWING_MODE_STATE_TOPIC, handle_swing_mode_received + ) + + @callback + def handle_onoff_mode_received( + msg: ReceiveMessage, template_name: str, attr: str + ) -> None: + """Handle receiving on/off mode via MQTT.""" + payload = render_template(msg, template_name) + payload_on: str = self._config[CONF_PAYLOAD_ON] + payload_off: str = self._config[CONF_PAYLOAD_OFF] + + if payload == "True": + payload = payload_on + elif payload == "False": + payload = payload_off + + if payload == payload_on: + setattr(self, attr, True) + elif payload == payload_off: + setattr(self, attr, False) + else: + _LOGGER.error("Invalid %s mode: %s", attr, payload) + + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_aux_mode_received(msg: ReceiveMessage) -> None: + """Handle receiving aux mode via MQTT.""" + handle_onoff_mode_received( + msg, CONF_AUX_STATE_TEMPLATE, "_attr_is_aux_heat" + ) + + add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_preset_mode_received(msg: ReceiveMessage) -> None: + """Handle receiving preset mode via MQTT.""" + preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) + if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: + self._attr_preset_mode = PRESET_NONE + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + return + if not preset_mode: + _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) + return + if not self.preset_modes or preset_mode not in self.preset_modes: + _LOGGER.warning( + "'%s' received on topic %s. '%s' is not a valid preset mode", + msg.payload, + msg.topic, + preset_mode, + ) + else: + self._attr_preset_mode = str(preset_mode) + + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + add_subscription( + topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received + ) + + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, self._sub_state, topics + ) + + async def _subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + + async def _publish(self, topic: str, payload: PublishPayloadType) -> None: + if self._topic[topic] is not None: + await self.async_publish( + self._topic[topic], + payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + self._config[CONF_ENCODING], + ) + + async def _set_temperature( + self, + temp: float | None, + cmnd_topic: str, + cmnd_template: str, + state_topic: str, + attr: str, + ) -> None: + if temp is not None: + if self._topic[state_topic] is None: + # optimistic mode + setattr(self, attr, temp) + + payload = self._command_templates[cmnd_template](temp) + await self._publish(cmnd_topic, payload) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperatures.""" + if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: + await self.async_set_hvac_mode(operation_mode) + + await self._set_temperature( + kwargs.get(ATTR_TEMPERATURE), + CONF_TEMP_COMMAND_TOPIC, + CONF_TEMP_COMMAND_TEMPLATE, + CONF_TEMP_STATE_TOPIC, + "_attr_target_temperature", + ) + + await self._set_temperature( + kwargs.get(ATTR_TARGET_TEMP_LOW), + CONF_TEMP_LOW_COMMAND_TOPIC, + CONF_TEMP_LOW_COMMAND_TEMPLATE, + CONF_TEMP_LOW_STATE_TOPIC, + "_attr_target_temperature_low", + ) + + await self._set_temperature( + kwargs.get(ATTR_TARGET_TEMP_HIGH), + CONF_TEMP_HIGH_COMMAND_TOPIC, + CONF_TEMP_HIGH_COMMAND_TEMPLATE, + CONF_TEMP_HIGH_STATE_TOPIC, + "_attr_target_temperature_high", + ) + + self.async_write_ha_state() + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new swing mode.""" + payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode) + await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) + + if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: + self._attr_swing_mode = swing_mode + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target temperature.""" + payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) + await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) + + if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: + self._attr_fan_mode = fan_mode + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new operation mode.""" + if hvac_mode == HVACMode.OFF: + await self._publish( + CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF] + ) + else: + await self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON]) + + payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode) + await self._publish(CONF_MODE_COMMAND_TOPIC, payload) + + if self._topic[CONF_MODE_STATE_TOPIC] is None: + self._attr_hvac_mode = hvac_mode + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set a preset mode.""" + if self._feature_preset_mode and self.preset_modes: + if preset_mode not in self.preset_modes and preset_mode is not PRESET_NONE: + _LOGGER.warning("'%s' is not a valid preset mode", preset_mode) + return + mqtt_payload = self._command_templates[CONF_PRESET_MODE_COMMAND_TEMPLATE]( + preset_mode + ) + await self._publish( + CONF_PRESET_MODE_COMMAND_TOPIC, + mqtt_payload, + ) + + if self._optimistic_preset_mode: + self._attr_preset_mode = preset_mode + self.async_write_ha_state() + + return + + async def _set_aux_heat(self, state: bool) -> None: + await self._publish( + CONF_AUX_COMMAND_TOPIC, + self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], + ) + + if self._topic[CONF_AUX_STATE_TOPIC] is None: + self._attr_is_aux_heat = state + self.async_write_ha_state() + + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self._set_aux_heat(True) + + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self._set_aux_heat(False) diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 8cfc3490f0c..88adcac7194 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -16,10 +16,10 @@ from .const import ( DEFAULT_QOS, DEFAULT_RETAIN, ) -from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic +from .util import valid_publish_topic, valid_qos_schema, valid_subscribe_topic SCHEMA_BASE = { - vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(CONF_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, } diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index ec818348701..b79ff30f111 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -27,6 +27,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads from homeassistant.helpers.selector import ( BooleanSelector, FileSelector, @@ -58,7 +60,10 @@ from .const import ( CONF_DISCOVERY_PREFIX, CONF_KEEPALIVE, CONF_TLS_INSECURE, + CONF_TRANSPORT, CONF_WILL_MESSAGE, + CONF_WS_HEADERS, + CONF_WS_PATH, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_ENCODING, @@ -66,14 +71,19 @@ from .const import ( DEFAULT_PORT, DEFAULT_PREFIX, DEFAULT_PROTOCOL, + DEFAULT_TRANSPORT, DEFAULT_WILL, + DEFAULT_WS_PATH, DOMAIN, SUPPORTED_PROTOCOLS, + SUPPORTED_TRANSPORTS, + TRANSPORT_TCP, + TRANSPORT_WEBSOCKETS, ) from .util import ( - MQTT_WILL_BIRTH_SCHEMA, async_create_certificate_temp_files, get_file_path, + valid_birth_will, valid_publish_topic, ) @@ -109,6 +119,15 @@ PROTOCOL_SELECTOR = SelectSelector( mode=SelectSelectorMode.DROPDOWN, ) ) +TRANSPORT_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=SUPPORTED_TRANSPORTS, + mode=SelectSelectorMode.DROPDOWN, + ) +) +WS_HEADERS_SELECTOR = TextSelector( + TextSelectorConfig(type=TextSelectorType.TEXT, multiline=True) +) CA_VERIFICATION_MODES = [ SelectOptionDict(value="off", label="Off"), SelectOptionDict(value="auto", label="Auto"), @@ -326,7 +345,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): CONF_BIRTH_MESSAGE, _birth_will("birth"), "bad_birth", - MQTT_WILL_BIRTH_SCHEMA, + valid_birth_will, ) if not user_input["birth_enable"]: options_config[CONF_BIRTH_MESSAGE] = {} @@ -336,7 +355,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): CONF_WILL_MESSAGE, _birth_will("will"), "bad_will", - MQTT_WILL_BIRTH_SCHEMA, + valid_birth_will, ) if not user_input["will_enable"]: options_config[CONF_WILL_MESSAGE] = {} @@ -482,8 +501,8 @@ async def async_get_broker_settings( return False certificate_id: str | None = user_input.get(CONF_CERTIFICATE) if certificate_id: - with process_uploaded_file(hass, certificate_id) as certiticate_file: - certificate = certiticate_file.read_text(encoding=DEFAULT_ENCODING) + with process_uploaded_file(hass, certificate_id) as certificate_file: + certificate = certificate_file.read_text(encoding=DEFAULT_ENCODING) # Return to form for file upload CA cert or client cert and key if ( @@ -493,14 +512,16 @@ async def async_get_broker_settings( or not certificate and user_input.get(SET_CA_CERT, "off") == "custom" and not certificate_id + or user_input.get(CONF_TRANSPORT) == TRANSPORT_WEBSOCKETS + and CONF_WS_PATH not in user_input ): return False if client_certificate_id: with process_uploaded_file( hass, client_certificate_id - ) as client_certiticate_file: - client_certificate = client_certiticate_file.read_text( + ) as client_certificate_file: + client_certificate = client_certificate_file.read_text( encoding=DEFAULT_ENCODING ) if client_key_id: @@ -526,6 +547,23 @@ async def async_get_broker_settings( del validated_user_input[SET_CA_CERT] if SET_CLIENT_CERT in validated_user_input: del validated_user_input[SET_CLIENT_CERT] + if validated_user_input.get(CONF_TRANSPORT, TRANSPORT_TCP) == TRANSPORT_TCP: + if CONF_WS_PATH in validated_user_input: + del validated_user_input[CONF_WS_PATH] + if CONF_WS_HEADERS in validated_user_input: + del validated_user_input[CONF_WS_HEADERS] + return True + try: + validated_user_input[CONF_WS_HEADERS] = json_loads( + validated_user_input.get(CONF_WS_HEADERS, "{}") + ) + schema = vol.Schema({cv.string: cv.template}) + schema(validated_user_input[CONF_WS_HEADERS]) + except JSON_DECODE_EXCEPTIONS + ( # pylint: disable=wrong-exception-operation + vol.MultipleInvalid, + ): + errors["base"] = "bad_ws_headers" + return False return True if user_input: @@ -562,6 +600,13 @@ async def async_get_broker_settings( current_client_key = current_config.get(CONF_CLIENT_KEY) current_tls_insecure = current_config.get(CONF_TLS_INSECURE, False) current_protocol = current_config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL) + current_transport = current_config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT) + current_ws_path = current_config.get(CONF_WS_PATH, DEFAULT_WS_PATH) + current_ws_headers = ( + json_dumps(current_config.get(CONF_WS_HEADERS)) + if CONF_WS_HEADERS in current_config + else None + ) advanced_broker_options |= bool( current_client_id or current_keepalive != DEFAULT_KEEPALIVE @@ -572,6 +617,7 @@ async def async_get_broker_settings( or current_protocol != DEFAULT_PROTOCOL or current_config.get(SET_CA_CERT, "off") != "off" or current_config.get(SET_CLIENT_CERT) + or current_transport == TRANSPORT_WEBSOCKETS ) # Build form @@ -665,6 +711,21 @@ async def async_get_broker_settings( description={"suggested_value": current_protocol}, ) ] = PROTOCOL_SELECTOR + fields[ + vol.Optional( + CONF_TRANSPORT, + description={"suggested_value": current_transport}, + ) + ] = TRANSPORT_SELECTOR + if current_transport == TRANSPORT_WEBSOCKETS: + fields[ + vol.Optional(CONF_WS_PATH, description={"suggested_value": current_ws_path}) + ] = TEXT_SELECTOR + fields[ + vol.Optional( + CONF_WS_HEADERS, description={"suggested_value": current_ws_headers} + ) + ] = WS_HEADERS_SELECTOR # Show form return False @@ -683,7 +744,11 @@ def try_connection( result: queue.Queue[bool] = queue.Queue(maxsize=1) def on_connect( - client_: mqtt.Client, userdata: None, flags: dict[str, Any], result_code: int + client_: mqtt.Client, + userdata: None, + flags: dict[str, Any], + result_code: int, + properties: mqtt.Properties | None = None, ) -> None: """Handle connection result.""" result.put(result_code == mqtt.CONNACK_ACCEPTED) @@ -704,10 +769,10 @@ def try_connection( def check_certicate_chain() -> str | None: """Check the MQTT certificates.""" - if client_certiticate := get_file_path(CONF_CLIENT_CERT): + if client_certificate := get_file_path(CONF_CLIENT_CERT): try: - with open(client_certiticate, "rb") as client_certiticate_file: - load_pem_x509_certificate(client_certiticate_file.read()) + with open(client_certificate, "rb") as client_certificate_file: + load_pem_x509_certificate(client_certificate_file.read()) except ValueError: return "bad_client_cert" # Check we can serialize the private key file @@ -719,9 +784,9 @@ def check_certicate_chain() -> str | None: return "bad_client_key" # Check the certificate chain context = SSLContext(PROTOCOL_TLS) - if client_certiticate and private_key: + if client_certificate and private_key: try: - context.load_cert_chain(client_certiticate, private_key) + context.load_cert_chain(client_certificate, private_key) except SSLError: return "bad_client_cert_key" # try to load the custom CA file diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 2be125c2c12..9db18af0718 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -32,14 +32,11 @@ from . import ( sensor as sensor_platform, siren as siren_platform, switch as switch_platform, + text as text_platform, update as update_platform, vacuum as vacuum_platform, ) from .const import ( - ATTR_PAYLOAD, - ATTR_QOS, - ATTR_RETAIN, - ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CERTIFICATE, @@ -49,19 +46,23 @@ from .const import ( CONF_KEEPALIVE, CONF_TLS_INSECURE, CONF_TLS_VERSION, + CONF_TRANSPORT, CONF_WILL_MESSAGE, + CONF_WS_HEADERS, + CONF_WS_PATH, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_KEEPALIVE, DEFAULT_PORT, DEFAULT_PREFIX, DEFAULT_PROTOCOL, - DEFAULT_QOS, - DEFAULT_RETAIN, + DEFAULT_TRANSPORT, DEFAULT_WILL, SUPPORTED_PROTOCOLS, + TRANSPORT_TCP, + TRANSPORT_WEBSOCKETS, ) -from .util import _VALID_QOS_SCHEMA, valid_publish_topic +from .util import valid_birth_will, valid_publish_topic DEFAULT_TLS_PROTOCOL = "auto" @@ -72,6 +73,7 @@ DEFAULT_VALUES = { CONF_PORT: DEFAULT_PORT, CONF_PROTOCOL: DEFAULT_PROTOCOL, CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_TRANSPORT: DEFAULT_TRANSPORT, CONF_WILL_MESSAGE: DEFAULT_WILL, CONF_KEEPALIVE: DEFAULT_KEEPALIVE, } @@ -129,6 +131,9 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( Platform.SWITCH.value: vol.All( cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] ), + Platform.TEXT.value: vol.All( + cv.ensure_list, [text_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), Platform.UPDATE.value: vol.All( cv.ensure_list, [update_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] ), @@ -144,16 +149,6 @@ CLIENT_KEY_AUTH_MSG = ( "the MQTT broker configuration" ) -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - CONFIG_SCHEMA_ENTRY = vol.Schema( { vol.Optional(CONF_CLIENT_ID): cv.string, @@ -170,12 +165,17 @@ CONFIG_SCHEMA_ENTRY = vol.Schema( vol.Optional(CONF_TLS_INSECURE): cv.boolean, vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_WILL_MESSAGE): valid_birth_will, + vol.Optional(CONF_BIRTH_MESSAGE): valid_birth_will, vol.Optional(CONF_DISCOVERY): cv.boolean, # discovery_prefix must be a valid publish topic because if no # state topic is specified, it will be created with the given prefix. vol.Optional(CONF_DISCOVERY_PREFIX): valid_publish_topic, + vol.Optional(CONF_TRANSPORT, default=DEFAULT_TRANSPORT): vol.All( + cv.string, vol.In([TRANSPORT_TCP, TRANSPORT_WEBSOCKETS]) + ), + vol.Optional(CONF_WS_PATH, default="/"): cv.string, + vol.Optional(CONF_WS_HEADERS, default={}): {cv.string: cv.string}, } ) @@ -197,8 +197,8 @@ CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( vol.Optional(CONF_TLS_INSECURE): cv.boolean, vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_WILL_MESSAGE): valid_birth_will, + vol.Optional(CONF_BIRTH_MESSAGE): valid_birth_will, vol.Optional(CONF_DISCOVERY): cv.boolean, # discovery_prefix must be a valid publish topic because if no # state topic is specified, it will be created with the given prefix. diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 1dc25c1e78c..f7fa93a36d0 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -19,9 +19,13 @@ CONF_ENCODING = "encoding" CONF_KEEPALIVE = "keepalive" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN +CONF_SCHEMA = "schema" CONF_STATE_TOPIC = "state_topic" CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" +CONF_TRANSPORT = "transport" +CONF_WS_PATH = "ws_path" +CONF_WS_HEADERS = "ws_headers" CONF_WILL_MESSAGE = "will_message" CONF_CERTIFICATE = "certificate" @@ -31,7 +35,6 @@ CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" DATA_MQTT = "mqtt" -MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" @@ -42,14 +45,21 @@ DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PORT = 1883 DEFAULT_RETAIN = False +DEFAULT_WS_PATH = "/" PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" -SUPPORTED_PROTOCOLS = [PROTOCOL_31, PROTOCOL_311] +PROTOCOL_5 = "5" +SUPPORTED_PROTOCOLS = [PROTOCOL_31, PROTOCOL_311, PROTOCOL_5] + +TRANSPORT_TCP = "tcp" +TRANSPORT_WEBSOCKETS = "websockets" +SUPPORTED_TRANSPORTS = [TRANSPORT_TCP, TRANSPORT_WEBSOCKETS] DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_TRANSPORT = TRANSPORT_TCP DEFAULT_BIRTH = { ATTR_TOPIC: DEFAULT_BIRTH_WILL_TOPIC, @@ -91,6 +101,7 @@ PLATFORMS = [ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.TEXT, Platform.UPDATE, Platform.VACUUM, ] @@ -112,6 +123,7 @@ RELOADABLE_PLATFORMS = [ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.TEXT, Platform.UPDATE, Platform.VACUUM, ] diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 7d7d4f61c4a..7f9b5dc3e85 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -31,6 +31,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -47,10 +48,9 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -113,44 +113,44 @@ MQTT_COVER_ATTRIBUTES_BLOCKED = frozenset( ) -def validate_options(value): +def validate_options(config: ConfigType) -> ConfigType: """Validate options. If set position topic is set then get position topic is set as well. """ - if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value: + if CONF_SET_POSITION_TOPIC in config and CONF_GET_POSITION_TOPIC not in config: raise vol.Invalid( f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) # if templates are set make sure the topic for the template is also set - if CONF_VALUE_TEMPLATE in value and CONF_STATE_TOPIC not in value: + if CONF_VALUE_TEMPLATE in config and CONF_STATE_TOPIC not in config: raise vol.Invalid( f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) - if CONF_GET_POSITION_TEMPLATE in value and CONF_GET_POSITION_TOPIC not in value: + if CONF_GET_POSITION_TEMPLATE in config and CONF_GET_POSITION_TOPIC not in config: raise vol.Invalid( f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) - if CONF_SET_POSITION_TEMPLATE in value and CONF_SET_POSITION_TOPIC not in value: + if CONF_SET_POSITION_TEMPLATE in config and CONF_SET_POSITION_TOPIC not in config: raise vol.Invalid( f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." ) - if CONF_TILT_COMMAND_TEMPLATE in value and CONF_TILT_COMMAND_TOPIC not in value: + if CONF_TILT_COMMAND_TEMPLATE in config and CONF_TILT_COMMAND_TOPIC not in config: raise vol.Invalid( f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." ) - if CONF_TILT_STATUS_TEMPLATE in value and CONF_TILT_STATUS_TOPIC not in value: + if CONF_TILT_STATUS_TEMPLATE in config and CONF_TILT_STATUS_TOPIC not in config: raise vol.Invalid( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." ) - return value + return config _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( @@ -204,10 +204,9 @@ PLATFORM_SCHEMA_MODERN = vol.All( validate_options, ) -# Configuring MQTT Covers under the cover platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Covers under the cover platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - validate_options, warn_for_legacy_schema(cover.DOMAIN), ) @@ -218,23 +217,6 @@ DISCOVERY_SCHEMA = vol.All( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT covers configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - cover.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -251,8 +233,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Cover.""" async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) @@ -261,26 +243,32 @@ async def _async_setup_entity( class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" - _entity_id_format = cover.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_COVER_ATTRIBUTES_BLOCKED + _entity_id_format: str = cover.ENTITY_ID_FORMAT + _attributes_extra_blocked: frozenset[str] = MQTT_COVER_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the cover.""" - self._position = None - self._state = None + self._position: int | None = None + self._state: str | None = None - self._optimistic = None - self._tilt_value = None - self._tilt_optimistic = None + self._optimistic: bool | None = None + self._tilt_value: int | None = None + self._tilt_optimistic: bool | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: no_position = ( config.get(CONF_SET_POSITION_TOPIC) is None and config.get(CONF_GET_POSITION_TOPIC) is None @@ -353,13 +341,13 @@ class MqttCover(MqttEntity, CoverEntity): config_attributes=template_config_attributes, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" topics = {} @callback @log_messages(self.hass, self.entity_id) - def tilt_message_received(msg): + def tilt_message_received(msg: ReceiveMessage) -> None: """Handle tilt updates.""" payload = self._tilt_status_template(msg.payload) @@ -371,7 +359,7 @@ class MqttCover(MqttEntity, CoverEntity): @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT state messages.""" payload = self._value_template(msg.payload) @@ -409,31 +397,32 @@ class MqttCover(MqttEntity, CoverEntity): @callback @log_messages(self.hass, self.entity_id) - def position_message_received(msg): + def position_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT position messages.""" - payload = self._get_position_template(msg.payload) + payload: ReceivePayloadType = self._get_position_template(msg.payload) + payload_dict: Any = None if not payload: _LOGGER.debug("Ignoring empty position message from '%s'", msg.topic) return try: - payload = json_loads(payload) + payload_dict = json_loads(payload) except JSON_DECODE_EXCEPTIONS: pass - if isinstance(payload, dict): - if "position" not in payload: + if payload_dict and isinstance(payload_dict, dict): + if "position" not in payload_dict: _LOGGER.warning( "Template (position_template) returned JSON without position attribute" ) return - if "tilt_position" in payload: + if "tilt_position" in payload_dict: if not self._config.get(CONF_TILT_STATE_OPTIMISTIC): # reset forced set tilt optimistic self._tilt_optimistic = False - self.tilt_payload_received(payload["tilt_position"]) - payload = payload["position"] + self.tilt_payload_received(payload_dict["tilt_position"]) + payload = payload_dict["position"] try: percentage_payload = self.find_percentage_in_range( @@ -481,7 +470,7 @@ class MqttCover(MqttEntity, CoverEntity): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -527,9 +516,9 @@ class MqttCover(MqttEntity, CoverEntity): return self._config.get(CONF_DEVICE_CLASS) @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" - supported_features = 0 + supported_features = CoverEntityFeature(0) if self._config.get(CONF_COMMAND_TOPIC) is not None: if self._config.get(CONF_PAYLOAD_OPEN) is not None: supported_features |= CoverEntityFeature.OPEN @@ -719,18 +708,20 @@ class MqttCover(MqttEntity, CoverEntity): else: await self.async_close_cover_tilt(**kwargs) - def is_tilt_closed(self): + def is_tilt_closed(self) -> bool: """Return if the cover is tilted closed.""" return self._tilt_value == self.find_percentage_in_range( float(self._config[CONF_TILT_CLOSED_POSITION]) ) - def find_percentage_in_range(self, position, range_type=TILT_PAYLOAD): + def find_percentage_in_range( + self, position: float, range_type: str = TILT_PAYLOAD + ) -> int: """Find the 0-100% value within the specified range.""" # the range of motion as defined by the min max values if range_type == COVER_PAYLOAD: - max_range = self._config[CONF_POSITION_OPEN] - min_range = self._config[CONF_POSITION_CLOSED] + max_range: int = self._config[CONF_POSITION_OPEN] + min_range: int = self._config[CONF_POSITION_CLOSED] else: max_range = self._config[CONF_TILT_MAX] min_range = self._config[CONF_TILT_MIN] @@ -745,7 +736,9 @@ class MqttCover(MqttEntity, CoverEntity): return position_percentage - def find_in_range_from_percent(self, percentage, range_type=TILT_PAYLOAD): + def find_in_range_from_percent( + self, percentage: float, range_type: str = TILT_PAYLOAD + ) -> int: """ Find the adjusted value for 0-100% within the specified range. @@ -755,8 +748,8 @@ class MqttCover(MqttEntity, CoverEntity): returning the offset """ if range_type == COVER_PAYLOAD: - max_range = self._config[CONF_POSITION_OPEN] - min_range = self._config[CONF_POSITION_CLOSED] + max_range: int = self._config[CONF_POSITION_OPEN] + min_range: int = self._config[CONF_POSITION_CLOSED] else: max_range = self._config[CONF_TILT_MAX] min_range = self._config[CONF_TILT_MIN] @@ -768,7 +761,7 @@ class MqttCover(MqttEntity, CoverEntity): return position @callback - def tilt_payload_received(self, _payload): + def tilt_payload_received(self, _payload: Any) -> None: """Set the tilt value.""" try: diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 5fae98eaea5..bdbdd74de96 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -28,7 +28,7 @@ def log_messages( debug_info_entities = get_mqtt_data(hass).debug_info_entities - def _log_message(msg): + def _log_message(msg: Any) -> None: """Log message.""" messages = debug_info_entities[entity_id]["subscriptions"][ msg.subscribed_topic diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 0646a5bda0c..a1bc2cdeb3e 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -1,9 +1,14 @@ """Provides device automations for MQTT.""" +from __future__ import annotations + import functools import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import device_trigger from .config import MQTT_BASE_SCHEMA @@ -20,14 +25,19 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( ).extend(MQTT_BASE_SCHEMA.schema) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up MQTT device automation dynamically through MQTT discovery.""" setup = functools.partial(_async_setup_automation, hass, config_entry=config_entry) await async_setup_entry_helper(hass, "device_automation", setup, PLATFORM_SCHEMA) -async def _async_setup_automation(hass, config, config_entry, discovery_data): +async def _async_setup_automation( + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType, +) -> None: """Set up an MQTT device automation.""" if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER: await device_trigger.async_setup_trigger( @@ -35,6 +45,6 @@ async def _async_setup_automation(hass, config, config_entry, discovery_data): ) -async def async_removed_from_device(hass, device_id): +async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None: """Handle Mqtt removed from a device.""" await device_trigger.async_removed_from_device(hass, device_id) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker.py similarity index 60% rename from homeassistant/components/mqtt/device_tracker/schema_discovery.py rename to homeassistant/components/mqtt/device_tracker.py index 1d3f9d109f6..26dc016e07e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -1,13 +1,17 @@ -"""Support for tracking MQTT enabled devices identified through discovery.""" +"""Support for tracking MQTT enabled devices identified.""" from __future__ import annotations +from collections.abc import Callable import functools import voluptuous as vol from homeassistant.components import device_tracker -from homeassistant.components.device_tracker import SOURCE_TYPES -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import ( + SOURCE_TYPES, + SourceType, + TrackerEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_GPS_ACCURACY, @@ -21,38 +25,51 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .. import subscription -from ..config import MQTT_RO_SCHEMA -from ..const import CONF_QOS, CONF_STATE_TOPIC -from ..debug_info import log_messages -from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper -from ..models import MqttValueTemplate -from ..util import get_mqtt_data +from . import subscription +from .config import MQTT_RO_SCHEMA +from .const import CONF_QOS, CONF_STATE_TOPIC +from .debug_info import log_messages +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + warn_for_legacy_schema, +) +from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType +from .util import get_mqtt_data CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" +DEFAULT_SOURCE_TYPE = SourceType.GPS + PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, - vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), + vol.Optional(CONF_SOURCE_TYPE, default=DEFAULT_SOURCE_TYPE): vol.In( + SOURCE_TYPES + ), } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Device Trackers under the device_tracker platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 +PLATFORM_SCHEMA = vol.All(warn_for_legacy_schema(device_tracker.DOMAIN)) -async def async_setup_entry_from_discovery( + +async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -63,8 +80,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Device Tracker entity.""" async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) @@ -74,37 +91,44 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): """Representation of a device tracker using MQTT.""" _entity_id_format = device_tracker.ENTITY_ID_FORMAT + _value_template: Callable[..., ReceivePayloadType] - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the tracker.""" - self._location_name = None - + self._location_name: str | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._value_template = MqttValueTemplate( - self._config.get(CONF_VALUE_TEMPLATE), entity=self + config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - payload = self._value_template(msg.payload) + payload: ReceivePayloadType = self._value_template(msg.payload) if payload == self._config[CONF_PAYLOAD_HOME]: self._location_name = STATE_HOME elif payload == self._config[CONF_PAYLOAD_NOT_HOME]: self._location_name = STATE_NOT_HOME else: + assert isinstance(msg.payload, str) self._location_name = msg.payload get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -121,46 +145,50 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def latitude(self): + def latitude(self) -> float | None: """Return latitude if provided in extra_state_attributes or None.""" if ( self.extra_state_attributes is not None and ATTR_LATITUDE in self.extra_state_attributes ): - return self.extra_state_attributes[ATTR_LATITUDE] + latitude: float = self.extra_state_attributes[ATTR_LATITUDE] + return latitude return None @property - def location_accuracy(self): + def location_accuracy(self) -> int: """Return location accuracy if provided in extra_state_attributes or None.""" if ( self.extra_state_attributes is not None and ATTR_GPS_ACCURACY in self.extra_state_attributes ): - return self.extra_state_attributes[ATTR_GPS_ACCURACY] - return None + accuracy: int = self.extra_state_attributes[ATTR_GPS_ACCURACY] + return accuracy + return 0 @property - def longitude(self): + def longitude(self) -> float | None: """Return longitude if provided in extra_state_attributes or None.""" if ( self.extra_state_attributes is not None and ATTR_LONGITUDE in self.extra_state_attributes ): - return self.extra_state_attributes[ATTR_LONGITUDE] + longitude: float = self.extra_state_attributes[ATTR_LONGITUDE] + return longitude return None @property - def location_name(self): + def location_name(self) -> str | None: """Return a location name for the current location of the device.""" return self._location_name @property - def source_type(self): + def source_type(self) -> SourceType | str: """Return the source type, eg gps or router, of the device.""" - return self._config.get(CONF_SOURCE_TYPE) + source_type: SourceType | str = self._config[CONF_SOURCE_TYPE] + return source_type diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py deleted file mode 100644 index 342817e38cc..00000000000 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Support for tracking MQTT enabled devices.""" -import voluptuous as vol - -from homeassistant.components import device_tracker -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from ..const import MQTT_DATA_DEVICE_TRACKER_LEGACY -from ..mixins import warn_for_legacy_schema -from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 -from .schema_discovery import async_setup_entry_from_discovery -from .schema_yaml import ( - PLATFORM_SCHEMA_YAML, - MQTTLegacyDeviceTrackerData, - async_setup_scanner_from_yaml, -) - -# Configuring MQTT Device Trackers under the device_tracker platform key is deprecated in HA Core 2022.6 -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN) -) - -# Legacy setup -async_setup_scanner = async_setup_scanner_from_yaml - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" - await async_setup_entry_from_discovery(hass, config_entry, async_add_entities) - # (re)load legacy service - if MQTT_DATA_DEVICE_TRACKER_LEGACY in hass.data: - yaml_device_tracker_data: MQTTLegacyDeviceTrackerData = hass.data[ - MQTT_DATA_DEVICE_TRACKER_LEGACY - ] - await async_setup_scanner_from_yaml( - hass, - config=yaml_device_tracker_data.config, - async_see=yaml_device_tracker_data.async_see, - ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py deleted file mode 100644 index c005a82dbeb..00000000000 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Support for tracking MQTT enabled devices defined in YAML.""" -from __future__ import annotations - -from collections.abc import Awaitable, Callable -import dataclasses -import logging -from typing import Any - -import voluptuous as vol - -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES -from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from ... import mqtt -from ..client import async_subscribe -from ..config import SCHEMA_BASE -from ..const import CONF_QOS, MQTT_DATA_DEVICE_TRACKER_LEGACY -from ..util import mqtt_config_entry_enabled, valid_subscribe_topic - -_LOGGER = logging.getLogger(__name__) - -CONF_PAYLOAD_HOME = "payload_home" -CONF_PAYLOAD_NOT_HOME = "payload_not_home" -CONF_SOURCE_TYPE = "source_type" - -PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( - { - vol.Required(CONF_DEVICES): {cv.string: valid_subscribe_topic}, - vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, - vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, - vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), - } -) - - -@dataclasses.dataclass -class MQTTLegacyDeviceTrackerData: - """Class to hold device tracker data.""" - - async_see: Callable[..., Awaitable[None]] - config: ConfigType - - -async def async_setup_scanner_from_yaml( - hass: HomeAssistant, - config: ConfigType, - async_see: Callable[..., Awaitable[None]], - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Set up the MQTT tracker.""" - devices = config[CONF_DEVICES] - qos = config[CONF_QOS] - payload_home = config[CONF_PAYLOAD_HOME] - payload_not_home = config[CONF_PAYLOAD_NOT_HOME] - source_type = config.get(CONF_SOURCE_TYPE) - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - subscriptions: list[Callable] = [] - - hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = MQTTLegacyDeviceTrackerData( - async_see, config - ) - if not mqtt_config_entry_enabled(hass): - _LOGGER.info( - "MQTT device trackers will be not available until the config entry is enabled", - ) - return False - - @callback - def _entry_unload(*_: Any) -> None: - """Handle the unload of the config entry.""" - # Unsubscribe from mqtt - for unsubscribe in subscriptions: - unsubscribe() - - for dev_id, topic in devices.items(): - - @callback - def async_message_received(msg, dev_id=dev_id): - """Handle received MQTT message.""" - if msg.payload == payload_home: - location_name = STATE_HOME - elif msg.payload == payload_not_home: - location_name = STATE_NOT_HOME - else: - location_name = msg.payload - - see_args = {"dev_id": dev_id, "location_name": location_name} - if source_type: - see_args["source_type"] = source_type - - hass.async_create_task(async_see(**see_args)) - - subscriptions.append( - await async_subscribe(hass, topic, async_message_received, qos) - ) - - config_entry.async_on_unload(_entry_unload) - - return True diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index f51731284cc..e8bcf1abc48 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import cast +from typing import Any, cast import attr import voluptuous as vol @@ -23,7 +23,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, trigger as mqtt_trigger from .config import MQTT_BASE_SCHEMA @@ -35,7 +35,7 @@ from .const import ( CONF_TOPIC, DOMAIN, ) -from .discovery import MQTT_DISCOVERY_DONE +from .discovery import MQTT_DISCOVERY_DONE, MQTTDiscoveryPayload from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, MqttDiscoveryDeviceUpdate, @@ -96,7 +96,7 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" - mqtt_config = { + mqtt_config: dict[str, Any] = { CONF_PLATFORM: DOMAIN, CONF_TOPIC: self.trigger.topic, CONF_ENCODING: DEFAULT_ENCODING, @@ -123,7 +123,7 @@ class Trigger: """Device trigger settings.""" device_id: str = attr.ib() - discovery_data: dict | None = attr.ib() + discovery_data: DiscoveryInfoType | None = attr.ib() hass: HomeAssistant = attr.ib() payload: str | None = attr.ib() qos: int | None = attr.ib() @@ -193,7 +193,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): hass: HomeAssistant, config: ConfigType, device_id: str, - discovery_data: dict, + discovery_data: DiscoveryInfoType, config_entry: ConfigEntry, ) -> None: """Initialize.""" @@ -237,7 +237,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): self.hass, discovery_hash, self.discovery_data, self.device_id ) - async def async_update(self, discovery_data: dict) -> None: + async def async_update(self, discovery_data: MQTTDiscoveryPayload) -> None: """Handle MQTT device trigger discovery updates.""" discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH] discovery_id = discovery_hash[1] @@ -261,11 +261,14 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): async def async_setup_trigger( - hass, config: ConfigType, config_entry: ConfigEntry, discovery_data: dict + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType, ) -> None: """Set up the MQTT device trigger.""" config = TRIGGER_DISCOVERY_SCHEMA(config) - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] if (device_id := update_device(hass, config_entry, config)) is None: async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 84f14d26146..2d460a69592 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -63,6 +63,7 @@ SUPPORTED_COMPONENTS = [ "sensor", "switch", "tag", + "text", "update", "vacuum", ] @@ -97,7 +98,7 @@ async def async_start( # noqa: C901 mqtt_data = get_mqtt_data(hass) mqtt_integrations = {} - async def async_discovery_message_received(msg) -> None: + async def async_discovery_message_received(msg: ReceiveMessage) -> None: """Process the received message.""" mqtt_data.last_discovery = time.time() payload = msg.payload @@ -122,46 +123,50 @@ async def async_start( # noqa: C901 if payload: try: - payload = json_loads(payload) + discovery_payload = MQTTDiscoveryPayload(json_loads(payload)) except ValueError: _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload) return + else: + discovery_payload = MQTTDiscoveryPayload({}) - payload = MQTTDiscoveryPayload(payload) - - for key in list(payload): + for key in list(discovery_payload): abbreviated_key = key key = ABBREVIATIONS.get(key, key) - payload[key] = payload.pop(abbreviated_key) + discovery_payload[key] = discovery_payload.pop(abbreviated_key) - if CONF_DEVICE in payload: - device = payload[CONF_DEVICE] + if CONF_DEVICE in discovery_payload: + device = discovery_payload[CONF_DEVICE] for key in list(device): abbreviated_key = key key = DEVICE_ABBREVIATIONS.get(key, key) device[key] = device.pop(abbreviated_key) - if CONF_AVAILABILITY in payload: - for availability_conf in cv.ensure_list(payload[CONF_AVAILABILITY]): + if CONF_AVAILABILITY in discovery_payload: + for availability_conf in cv.ensure_list( + discovery_payload[CONF_AVAILABILITY] + ): if isinstance(availability_conf, dict): for key in list(availability_conf): abbreviated_key = key key = ABBREVIATIONS.get(key, key) availability_conf[key] = availability_conf.pop(abbreviated_key) - if TOPIC_BASE in payload: - base = payload.pop(TOPIC_BASE) - for key, value in payload.items(): + if TOPIC_BASE in discovery_payload: + base = discovery_payload.pop(TOPIC_BASE) + for key, value in discovery_payload.items(): if isinstance(value, str) and value: if value[0] == TOPIC_BASE and key.endswith("topic"): - payload[key] = f"{base}{value[1:]}" + discovery_payload[key] = f"{base}{value[1:]}" if value[-1] == TOPIC_BASE and key.endswith("topic"): - payload[key] = f"{value[:-1]}{base}" - if payload.get(CONF_AVAILABILITY): - for availability_conf in cv.ensure_list(payload[CONF_AVAILABILITY]): + discovery_payload[key] = f"{value[:-1]}{base}" + if discovery_payload.get(CONF_AVAILABILITY): + for availability_conf in cv.ensure_list( + discovery_payload[CONF_AVAILABILITY] + ): if not isinstance(availability_conf, dict): continue - if topic := availability_conf.get(CONF_TOPIC): + if topic := str(availability_conf.get(CONF_TOPIC)): if topic[0] == TOPIC_BASE: availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" if topic[-1] == TOPIC_BASE: @@ -171,21 +176,25 @@ async def async_start( # noqa: C901 discovery_id = " ".join((node_id, object_id)) if node_id else object_id discovery_hash = (component, discovery_id) - if payload: + if discovery_payload: # Attach MQTT topic to the payload, used for debug prints - setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')") + setattr( + discovery_payload, + "__configuration_source__", + f"MQTT (topic: '{topic}')", + ) discovery_data = { ATTR_DISCOVERY_HASH: discovery_hash, - ATTR_DISCOVERY_PAYLOAD: payload, + ATTR_DISCOVERY_PAYLOAD: discovery_payload, ATTR_DISCOVERY_TOPIC: topic, } - setattr(payload, "discovery_data", discovery_data) + setattr(discovery_payload, "discovery_data", discovery_data) - payload[CONF_PLATFORM] = "mqtt" + discovery_payload[CONF_PLATFORM] = "mqtt" if discovery_hash in mqtt_data.discovery_pending_discovered: pending = mqtt_data.discovery_pending_discovered[discovery_hash]["pending"] - pending.appendleft(payload) + pending.appendleft(discovery_payload) _LOGGER.info( "Component has already been discovered: %s %s, queuing update", component, @@ -193,7 +202,9 @@ async def async_start( # noqa: C901 ) return - await async_process_discovery_payload(component, discovery_id, payload) + await async_process_discovery_payload( + component, discovery_id, discovery_payload + ) async def async_process_discovery_payload( component: str, discovery_id: str, payload: MQTTDiscoveryPayload @@ -204,7 +215,7 @@ async def async_start( # noqa: C901 discovery_hash = (component, discovery_id) if discovery_hash in mqtt_data.discovery_already_discovered or payload: - async def discovery_done(_) -> None: + async def discovery_done(_: Any) -> None: pending = mqtt_data.discovery_pending_discovered[discovery_hash][ "pending" ] diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 866b429c68f..da061da14e4 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,6 +1,7 @@ """Support for MQTT fans.""" from __future__ import annotations +from collections.abc import Callable import functools import logging import math @@ -27,6 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.percentage import ( int_states_in_range, @@ -51,10 +53,15 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" @@ -70,20 +77,12 @@ CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" CONF_PRESET_MODES_LIST = "preset_modes" CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" -CONF_SPEED_STATE_TOPIC = "speed_state_topic" -CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" -CONF_SPEED_VALUE_TEMPLATE = "speed_value_template" CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" -CONF_PAYLOAD_OFF_SPEED = "payload_off_speed" -CONF_PAYLOAD_LOW_SPEED = "payload_low_speed" -CONF_PAYLOAD_MEDIUM_SPEED = "payload_medium_speed" -CONF_PAYLOAD_HIGH_SPEED = "payload_high_speed" -CONF_SPEED_LIST = "speeds" DEFAULT_NAME = "MQTT Fan" DEFAULT_PAYLOAD_ON = "ON" @@ -110,18 +109,18 @@ MQTT_FAN_ATTRIBUTES_BLOCKED = frozenset( _LOGGER = logging.getLogger(__name__) -def valid_speed_range_configuration(config): +def valid_speed_range_configuration(config: ConfigType) -> ConfigType: """Validate that the fan speed_range configuration is valid, throws if it isn't.""" - if config.get(CONF_SPEED_RANGE_MIN) == 0: + if config[CONF_SPEED_RANGE_MIN] == 0: raise ValueError("speed_range_min must be > 0") - if config.get(CONF_SPEED_RANGE_MIN) >= config.get(CONF_SPEED_RANGE_MAX): + if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]: raise ValueError("speed_range_max must be > speed_range_min") return config -def valid_preset_mode_configuration(config): +def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the preset mode reset payload is not one of the preset modes.""" - if config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config.get(CONF_PRESET_MODES_LIST): + if config[CONF_PAYLOAD_RESET_PRESET_MODE] in config[CONF_PRESET_MODES_LIST]: raise ValueError("preset_modes must not contain payload_reset_preset_mode") return config @@ -169,71 +168,29 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional( CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD ): cv.string, - vol.Optional(CONF_SPEED_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_SPEED_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Fans under the fan platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Fans under the fan platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - valid_speed_range_configuration, - valid_preset_mode_configuration, warn_for_legacy_schema(fan.DOMAIN), ) PLATFORM_SCHEMA_MODERN = vol.All( - # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and - # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, - # are no longer supported, support was removed in release 2021.12 - cv.removed(CONF_PAYLOAD_HIGH_SPEED), - cv.removed(CONF_PAYLOAD_LOW_SPEED), - cv.removed(CONF_PAYLOAD_MEDIUM_SPEED), - cv.removed(CONF_SPEED_COMMAND_TOPIC), - cv.removed(CONF_SPEED_LIST), - cv.removed(CONF_SPEED_STATE_TOPIC), - cv.removed(CONF_SPEED_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE, valid_speed_range_configuration, valid_preset_mode_configuration, ) DISCOVERY_SCHEMA = vol.All( - # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and - # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, - # are no longer supported, support was removed in release 2021.12 - cv.removed(CONF_PAYLOAD_HIGH_SPEED), - cv.removed(CONF_PAYLOAD_LOW_SPEED), - cv.removed(CONF_PAYLOAD_MEDIUM_SPEED), - cv.removed(CONF_SPEED_COMMAND_TOPIC), - cv.removed(CONF_SPEED_LIST), - cv.removed(CONF_SPEED_STATE_TOPIC), - cv.removed(CONF_SPEED_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_speed_range_configuration, valid_preset_mode_configuration, ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT fans configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - fan.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -250,8 +207,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT fan.""" async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) @@ -263,35 +220,41 @@ class MqttFan(MqttEntity, FanEntity): _entity_id_format = fan.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): - """Initialize the MQTT fan.""" - self._state = None - self._percentage = None - self._preset_mode = None - self._oscillation = None - self._supported_features = 0 + _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] + _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _feature_percentage: bool + _feature_preset_mode: bool + _topic: dict[str, Any] + _optimistic: bool + _optimistic_oscillation: bool + _optimistic_percentage: bool + _optimistic_preset_mode: bool + _payload: dict[str, Any] + _speed_range: tuple[int, int] - self._topic = None - self._payload = None - self._value_templates = None - self._command_templates = None - self._optimistic = None - self._optimistic_oscillation = None - self._optimistic_percentage = None - self._optimistic_preset_mode = None + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the MQTT fan.""" + self._attr_percentage = None + self._attr_preset_mode = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._speed_range = ( - config.get(CONF_SPEED_RANGE_MIN), - config.get(CONF_SPEED_RANGE_MAX), + config[CONF_SPEED_RANGE_MIN], + config[CONF_SPEED_RANGE_MAX], ) self._topic = { key: config.get(key) @@ -306,18 +269,6 @@ class MqttFan(MqttEntity, FanEntity): CONF_OSCILLATION_COMMAND_TOPIC, ) } - self._value_templates = { - CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), - ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE), - ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE), - ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE), - } - self._command_templates = { - CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), - ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE), - ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE), - ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE), - } self._payload = { "STATE_ON": config[CONF_PAYLOAD_ON], "STATE_OFF": config[CONF_PAYLOAD_OFF], @@ -330,11 +281,11 @@ class MqttFan(MqttEntity, FanEntity): self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: - self._preset_modes = config[CONF_PRESET_MODES_LIST] + self._attr_preset_modes = config[CONF_PRESET_MODES_LIST] else: - self._preset_modes = [] + self._attr_preset_modes = [] - self._speed_count = ( + self._attr_speed_count = ( min(int_states_in_range(self._speed_range), 100) if self._feature_percentage else 100 @@ -352,45 +303,59 @@ class MqttFan(MqttEntity, FanEntity): optimistic or self._topic[CONF_PRESET_MODE_STATE_TOPIC] is None ) - self._supported_features = 0 - self._supported_features |= ( + self._attr_supported_features = FanEntityFeature(0) + self._attr_supported_features |= ( self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None and FanEntityFeature.OSCILLATE ) if self._feature_percentage: - self._supported_features |= FanEntityFeature.SET_SPEED + self._attr_supported_features |= FanEntityFeature.SET_SPEED if self._feature_preset_mode: - self._supported_features |= FanEntityFeature.PRESET_MODE + self._attr_supported_features |= FanEntityFeature.PRESET_MODE - for key, tpl in self._command_templates.items(): + command_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), + ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE), + ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE), + ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE), + } + self._command_templates = {} + for key, tpl in command_templates.items(): self._command_templates[key] = MqttCommandTemplate( tpl, entity=self ).async_render - for key, tpl in self._value_templates.items(): + self._value_templates = {} + value_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE), + ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE), + ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE), + } + for key, tpl in value_templates.items(): self._value_templates[key] = MqttValueTemplate( tpl, entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, Any] = {} @callback @log_messages(self.hass, self.entity_id) - def state_received(msg): + def state_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message.""" payload = self._value_templates[CONF_STATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) return if payload == self._payload["STATE_ON"]: - self._state = True + self._attr_is_on = True elif payload == self._payload["STATE_OFF"]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: @@ -403,7 +368,7 @@ class MqttFan(MqttEntity, FanEntity): @callback @log_messages(self.hass, self.entity_id) - def percentage_received(msg): + def percentage_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for the percentage.""" rendered_percentage_payload = self._value_templates[ATTR_PERCENTAGE]( msg.payload @@ -412,7 +377,7 @@ class MqttFan(MqttEntity, FanEntity): _LOGGER.debug("Ignoring empty speed from '%s'", msg.topic) return if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: - self._percentage = None + self._attr_percentage = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: @@ -435,7 +400,7 @@ class MqttFan(MqttEntity, FanEntity): rendered_percentage_payload, ) return - self._percentage = percentage + self._attr_percentage = percentage get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None: @@ -445,21 +410,21 @@ class MqttFan(MqttEntity, FanEntity): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._percentage = None + self._attr_percentage = None @callback @log_messages(self.hass, self.entity_id) - def preset_mode_received(msg): + def preset_mode_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for preset mode.""" - preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) + preset_mode = str(self._value_templates[ATTR_PRESET_MODE](msg.payload)) if preset_mode == self._payload["PRESET_MODE_RESET"]: - self._preset_mode = None + self._attr_preset_mode = None self.async_write_ha_state() return if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) return - if preset_mode not in self.preset_modes: + if not self.preset_modes or preset_mode not in self.preset_modes: _LOGGER.warning( "'%s' received on topic %s. '%s' is not a valid preset mode", msg.payload, @@ -468,7 +433,7 @@ class MqttFan(MqttEntity, FanEntity): ) return - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: @@ -478,20 +443,20 @@ class MqttFan(MqttEntity, FanEntity): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._preset_mode = None + self._attr_preset_mode = None @callback @log_messages(self.hass, self.entity_id) - def oscillation_received(msg): + def oscillation_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for the oscillation.""" payload = self._value_templates[ATTR_OSCILLATING](msg.payload) if not payload: _LOGGER.debug("Ignoring empty oscillation from '%s'", msg.topic) return if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: - self._oscillation = True + self._attr_oscillating = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: - self._oscillation = False + self._attr_oscillating = False get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: @@ -501,13 +466,13 @@ class MqttFan(MqttEntity, FanEntity): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._oscillation = False + self._attr_oscillating = False self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -519,39 +484,9 @@ class MqttFan(MqttEntity, FanEntity): @property def is_on(self) -> bool | None: """Return true if device is on.""" - return self._state + # The default for FanEntity is to compute it based on percentage + return self._attr_is_on - @property - def percentage(self) -> int | None: - """Return the current percentage.""" - return self._percentage - - @property - def preset_mode(self) -> str | None: - """Return the current preset _mode.""" - return self._preset_mode - - @property - def preset_modes(self) -> list[str]: - """Get the list of available preset modes.""" - return self._preset_modes - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return self._speed_count - - @property - def oscillating(self) -> bool | None: - """Return the oscillation state.""" - return self._oscillation - - # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7) async def async_turn_on( self, percentage: int | None = None, @@ -575,7 +510,7 @@ class MqttFan(MqttEntity, FanEntity): if preset_mode: await self.async_set_preset_mode(preset_mode) if self._optimistic: - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -592,7 +527,7 @@ class MqttFan(MqttEntity, FanEntity): self._config[CONF_ENCODING], ) if self._optimistic: - self._state = False + self._attr_is_on = False self.async_write_ha_state() async def async_set_percentage(self, percentage: int) -> None: @@ -613,7 +548,7 @@ class MqttFan(MqttEntity, FanEntity): ) if self._optimistic_percentage: - self._percentage = percentage + self._attr_percentage = percentage self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -634,7 +569,7 @@ class MqttFan(MqttEntity, FanEntity): ) if self._optimistic_preset_mode: - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode self.async_write_ha_state() async def async_oscillate(self, oscillating: bool) -> None: @@ -660,5 +595,5 @@ class MqttFan(MqttEntity, FanEntity): ) if self._optimistic_oscillation: - self._oscillation = oscillating + self._attr_oscillating = oscillating self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 7514f0ff672..38d3e46ea45 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -1,6 +1,7 @@ """Support for MQTT humidifiers.""" from __future__ import annotations +from collections.abc import Callable import functools import logging from typing import Any @@ -28,6 +29,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -47,10 +49,15 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" @@ -87,18 +94,18 @@ MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED = frozenset( _LOGGER = logging.getLogger(__name__) -def valid_mode_configuration(config): +def valid_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the mode reset payload is not one of the available modes.""" - if config.get(CONF_PAYLOAD_RESET_MODE) in config.get(CONF_AVAILABLE_MODES_LIST): + if config[CONF_PAYLOAD_RESET_MODE] in config[CONF_AVAILABLE_MODES_LIST]: raise ValueError("modes must not contain payload_reset_mode") return config -def valid_humidity_range_configuration(config): +def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: """Validate that the target_humidity range configuration is valid, throws if it isn't.""" - if config.get(CONF_TARGET_HUMIDITY_MIN) >= config.get(CONF_TARGET_HUMIDITY_MAX): + if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]: raise ValueError("target_humidity_max must be > target_humidity_min") - if config.get(CONF_TARGET_HUMIDITY_MAX) > 100: + if config[CONF_TARGET_HUMIDITY_MAX] > 100: raise ValueError("max_humidity must be <= 100") return config @@ -142,11 +149,9 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Humidifiers under the humidifier platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Humidifiers under the humidifier platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - valid_humidity_range_configuration, - valid_mode_configuration, warn_for_legacy_schema(humidifier.DOMAIN), ) @@ -163,23 +168,6 @@ DISCOVERY_SCHEMA = vol.All( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT humidifier configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - humidifier.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -196,8 +184,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT humidifier.""" async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)]) @@ -209,33 +197,36 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): _entity_id_format = humidifier.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): - """Initialize the MQTT humidifier.""" - self._state = None - self._target_humidity = None - self._mode = None - self._supported_features = 0 + _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] + _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _optimistic: bool + _optimistic_target_humidity: bool + _optimistic_mode: bool + _payload: dict[str, str] + _topic: dict[str, Any] - self._topic = None - self._payload = None - self._value_templates = None - self._command_templates = None - self._optimistic = None - self._optimistic_target_humidity = None - self._optimistic_mode = None + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the MQTT humidifier.""" + self._attr_mode = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._attr_min_humidity = config.get(CONF_TARGET_HUMIDITY_MIN) - self._attr_max_humidity = config.get(CONF_TARGET_HUMIDITY_MAX) + self._attr_min_humidity = config[CONF_TARGET_HUMIDITY_MIN] + self._attr_max_humidity = config[CONF_TARGET_HUMIDITY_MAX] self._topic = { key: config.get(key) @@ -248,16 +239,6 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): CONF_MODE_COMMAND_TOPIC, ) } - self._value_templates = { - CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), - ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE), - ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE), - } - self._command_templates = { - CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), - ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE), - ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE), - } self._payload = { "STATE_ON": config[CONF_PAYLOAD_ON], "STATE_OFF": config[CONF_PAYLOAD_OFF], @@ -265,50 +246,60 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): "MODE_RESET": config[CONF_PAYLOAD_RESET_MODE], } if CONF_MODE_COMMAND_TOPIC in config and CONF_AVAILABLE_MODES_LIST in config: - self._available_modes = config[CONF_AVAILABLE_MODES_LIST] + self._attr_available_modes = config[CONF_AVAILABLE_MODES_LIST] else: - self._available_modes = [] - if self._available_modes: + self._attr_available_modes = [] + if self._attr_available_modes: self._attr_supported_features = HumidifierEntityFeature.MODES - else: - self._attr_supported_features = 0 - optimistic = config[CONF_OPTIMISTIC] + optimistic: bool = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None self._optimistic_target_humidity = ( optimistic or self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is None ) self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None - for key, tpl in self._command_templates.items(): + self._command_templates = {} + command_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), + ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE), + ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE), + } + for key, tpl in command_templates.items(): self._command_templates[key] = MqttCommandTemplate( tpl, entity=self ).async_render - for key, tpl in self._value_templates.items(): + self._value_templates = {} + value_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE), + ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE), + } + for key, tpl in value_templates.items(): self._value_templates[key] = MqttValueTemplate( tpl, entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, Any] = {} @callback @log_messages(self.hass, self.entity_id) - def state_received(msg): + def state_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message.""" payload = self._value_templates[CONF_STATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) return if payload == self._payload["STATE_ON"]: - self._state = True + self._attr_is_on = True elif payload == self._payload["STATE_OFF"]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: @@ -321,7 +312,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): @callback @log_messages(self.hass, self.entity_id) - def target_humidity_received(msg): + def target_humidity_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for the target humidity.""" rendered_target_humidity_payload = self._value_templates[ATTR_HUMIDITY]( msg.payload @@ -330,7 +321,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): _LOGGER.debug("Ignoring empty target humidity from '%s'", msg.topic) return if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: - self._target_humidity = None + self._attr_target_humidity = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: @@ -354,7 +345,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): rendered_target_humidity_payload, ) return - self._target_humidity = target_humidity + self._attr_target_humidity = target_humidity get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None: @@ -364,21 +355,21 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._target_humidity = None + self._attr_target_humidity = None @callback @log_messages(self.hass, self.entity_id) - def mode_received(msg): + def mode_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for mode.""" - mode = self._value_templates[ATTR_MODE](msg.payload) + mode = str(self._value_templates[ATTR_MODE](msg.payload)) if mode == self._payload["MODE_RESET"]: - self._mode = None + self._attr_mode = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not mode: _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) return - if mode not in self.available_modes: + if not self.available_modes or mode not in self.available_modes: _LOGGER.warning( "'%s' received on topic %s. '%s' is not a valid mode", msg.payload, @@ -387,7 +378,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) return - self._mode = mode + self._attr_mode = mode get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_MODE_STATE_TOPIC] is not None: @@ -397,13 +388,13 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._mode = None + self._attr_mode = None self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -412,26 +403,6 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): """Return true if we do optimistic updates.""" return self._optimistic - @property - def available_modes(self) -> list: - """Get the list of available modes.""" - return self._available_modes - - @property - def is_on(self) -> bool | None: - """Return true if device is on.""" - return self._state - - @property - def target_humidity(self): - """Return the current target humidity.""" - return self._target_humidity - - @property - def mode(self): - """Return the current mode.""" - return self._mode - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the entity. @@ -446,7 +417,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._config[CONF_ENCODING], ) if self._optimistic: - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -463,7 +434,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._config[CONF_ENCODING], ) if self._optimistic: - self._state = False + self._attr_is_on = False self.async_write_ha_state() async def async_set_humidity(self, humidity: int) -> None: @@ -481,7 +452,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) if self._optimistic_target_humidity: - self._target_humidity = humidity + self._attr_target_humidity = humidity self.async_write_ha_state() async def async_set_mode(self, mode: str) -> None: @@ -489,7 +460,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): This method is a coroutine. """ - if mode not in self.available_modes: + if not self.available_modes or mode not in self.available_modes: _LOGGER.warning("'%s'is not a valid mode", mode) return @@ -504,5 +475,5 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) if self._optimistic_mode: - self._mode = mode + self._attr_mode = mode self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index b7d52919d5e..e7b2dcf5ae4 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,23 +1,18 @@ """Support for MQTT lights.""" from __future__ import annotations -from collections.abc import Callable import functools +from typing import Any import voluptuous as vol from homeassistant.components import light from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType -from ..mixins import ( - async_setup_entry_helper, - async_setup_platform_helper, - warn_for_legacy_schema, -) +from ..mixins import async_setup_entry_helper, warn_for_legacy_schema from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( DISCOVERY_SCHEMA_BASIC, @@ -39,34 +34,37 @@ from .schema_template import ( ) -def validate_mqtt_light_discovery(value): +def validate_mqtt_light_discovery(config_value: dict[str, Any]) -> ConfigType: """Validate MQTT light schema for.""" schemas = { "basic": DISCOVERY_SCHEMA_BASIC, "json": DISCOVERY_SCHEMA_JSON, "template": DISCOVERY_SCHEMA_TEMPLATE, } - return schemas[value[CONF_SCHEMA]](value) + config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) + return config -def validate_mqtt_light(value): +def validate_mqtt_light(config_value: dict[str, Any]) -> ConfigType: """Validate MQTT light schema.""" schemas = { "basic": PLATFORM_SCHEMA_BASIC, "json": PLATFORM_SCHEMA_JSON, "template": PLATFORM_SCHEMA_TEMPLATE, } - return schemas[value[CONF_SCHEMA]](value) + config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) + return config -def validate_mqtt_light_modern(value): +def validate_mqtt_light_modern(config_value: dict[str, Any]) -> ConfigType: """Validate MQTT light schema.""" schemas = { "basic": PLATFORM_SCHEMA_MODERN_BASIC, "json": PLATFORM_SCHEMA_MODERN_JSON, "template": PLATFORM_SCHEMA_MODERN_TEMPLATE, } - return schemas[value[CONF_SCHEMA]](value) + config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) + return config DISCOVERY_SCHEMA = vol.All( @@ -74,10 +72,9 @@ DISCOVERY_SCHEMA = vol.All( validate_mqtt_light_discovery, ) -# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Lights under the light platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema, extra=vol.ALLOW_EXTRA), - validate_mqtt_light, warn_for_legacy_schema(light.DOMAIN), ) @@ -87,23 +84,6 @@ PLATFORM_SCHEMA_MODERN = vol.All( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT light through configuration.yaml (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - light.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -120,11 +100,11 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, + config_entry: ConfigEntry, discovery_data: dict | None = None, ) -> None: """Set up a MQTT Light.""" - setup_entity: dict[str, Callable] = { + setup_entity = { "basic": async_setup_entity_basic, "json": async_setup_entity_json, "template": async_setup_entity_template, diff --git a/homeassistant/components/mqtt/light/schema.py b/homeassistant/components/mqtt/light/schema.py index a7ab5e986a7..6e2ac60b28d 100644 --- a/homeassistant/components/mqtt/light/schema.py +++ b/homeassistant/components/mqtt/light/schema.py @@ -1,7 +1,7 @@ """Shared schema code.""" import voluptuous as vol -CONF_SCHEMA = "schema" +from ..const import CONF_SCHEMA MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index d435d4e91ad..d2f8a5ac03e 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -1,5 +1,9 @@ """Support for MQTT lights.""" +from __future__ import annotations + +from collections.abc import Callable import logging +from typing import Any, cast import voluptuous as vol @@ -24,6 +28,7 @@ from homeassistant.components.light import ( LightEntityFeature, valid_supported_color_modes, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, @@ -32,9 +37,11 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util from .. import subscription @@ -50,7 +57,16 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity -from ..models import MqttCommandTemplate, MqttValueTemplate +from ..models import ( + MessageCallbackType, + MqttCommandTemplate, + MqttValueTemplate, + PayloadSentinel, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, + TemplateVarsType, +) from ..util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA @@ -208,7 +224,7 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) -# The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 +# The use of PLATFORM_SCHEMA was deprecated in HA Core 2022.6 PLATFORM_SCHEMA_BASIC = vol.All( cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) @@ -237,8 +253,12 @@ PLATFORM_SCHEMA_MODERN_BASIC = vol.All( async def async_setup_entity_basic( - hass, config, async_add_entities, config_entry, discovery_data=None -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, +) -> None: """Set up a MQTT Light.""" async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) @@ -248,49 +268,50 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED + _topic: dict[str, str | None] + _payload: dict[str, str] + _command_templates: dict[ + str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType] + ] + _value_templates: dict[ + str, Callable[[ReceivePayloadType, ReceivePayloadType], ReceivePayloadType] + ] + _optimistic: bool + _optimistic_brightness: bool + _optimistic_color_mode: bool + _optimistic_color_temp: bool + _optimistic_effect: bool + _optimistic_hs_color: bool + _optimistic_rgb_color: bool + _optimistic_rgbw_color: bool + _optimistic_rgbww_color: bool + _optimistic_xy_color: bool - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize MQTT light.""" - self._brightness = None - self._color_mode = None - self._color_temp = None - self._effect = None - self._hs_color = None - self._rgb_color = None - self._rgbw_color = None - self._rgbww_color = None - self._state = None - self._supported_color_modes = None - self._xy_color = None - - self._topic = None - self._payload = None - self._command_templates = None - self._value_templates = None - self._optimistic = False - self._optimistic_brightness = False - self._optimistic_color_mode = False - self._optimistic_color_temp = False - self._optimistic_effect = False - self._optimistic_hs_color = False - self._optimistic_rgb_color = False - self._optimistic_rgbw_color = False - self._optimistic_rgbww_color = False - self._optimistic_xy_color = False - MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA_BASIC - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_min_mireds = config.get(CONF_MIN_MIREDS, super().min_mireds) + self._attr_max_mireds = config.get(CONF_MAX_MIREDS, super().max_mireds) + self._attr_effect_list = config.get(CONF_EFFECT_LIST) + if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] - topic = { + topic: dict[str, str | None] = { key: config.get(key) for key in ( CONF_BRIGHTNESS_COMMAND_TOPIC, @@ -318,32 +339,19 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._topic = topic self._payload = {"on": config[CONF_PAYLOAD_ON], "off": config[CONF_PAYLOAD_OFF]} - value_templates = {} - for key in VALUE_TEMPLATE_KEYS: - value_templates[key] = None - if CONF_VALUE_TEMPLATE in config: - value_templates = { - key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS - } - for key in VALUE_TEMPLATE_KEYS & config.keys(): - value_templates[key] = config[key] self._value_templates = { key: MqttValueTemplate( - template, entity=self + config.get(key), entity=self ).async_render_with_possible_json_value - for key, template in value_templates.items() + for key in VALUE_TEMPLATE_KEYS } - command_templates = {} - for key in COMMAND_TEMPLATE_KEYS: - command_templates[key] = None - for key in COMMAND_TEMPLATE_KEYS & config.keys(): - command_templates[key] = MqttCommandTemplate( - config[key], entity=self - ).async_render - self._command_templates = command_templates + self._command_templates = { + key: MqttCommandTemplate(config.get(key), entity=self).async_render + for key in COMMAND_TEMPLATE_KEYS + } - optimistic = config[CONF_OPTIMISTIC] + optimistic: bool = config[CONF_OPTIMISTIC] self._optimistic_color_mode = ( optimistic or topic[CONF_COLOR_MODE_STATE_TOPIC] is None ) @@ -370,50 +378,57 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._optimistic_effect = optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None self._optimistic_hs_color = optimistic or topic[CONF_HS_STATE_TOPIC] is None self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None - supported_color_modes = set() + supported_color_modes: set[ColorMode] = set() if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.COLOR_TEMP) - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP if topic[CONF_HS_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.HS) - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS if topic[CONF_RGB_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.RGB) - self._color_mode = ColorMode.RGB + self._attr_color_mode = ColorMode.RGB if topic[CONF_RGBW_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.RGBW) - self._color_mode = ColorMode.RGBW + self._attr_color_mode = ColorMode.RGBW if topic[CONF_RGBWW_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.RGBWW) - self._color_mode = ColorMode.RGBWW + self._attr_color_mode = ColorMode.RGBWW if topic[CONF_WHITE_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.WHITE) if topic[CONF_XY_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.XY) - self._color_mode = ColorMode.XY + self._attr_color_mode = ColorMode.XY if len(supported_color_modes) > 1: - self._color_mode = ColorMode.UNKNOWN + self._attr_color_mode = ColorMode.UNKNOWN if not supported_color_modes: if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: - self._color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = ColorMode.BRIGHTNESS supported_color_modes.add(ColorMode.BRIGHTNESS) else: - self._color_mode = ColorMode.ONOFF + self._attr_color_mode = ColorMode.ONOFF supported_color_modes.add(ColorMode.ONOFF) # Validate the color_modes configuration - self._supported_color_modes = valid_supported_color_modes(supported_color_modes) + self._attr_supported_color_modes = valid_supported_color_modes( + supported_color_modes + ) - def _is_optimistic(self, attribute): + self._attr_supported_features = LightEntityFeature(0) + if topic[CONF_EFFECT_COMMAND_TOPIC] is not None: + self._attr_supported_features |= LightEntityFeature.EFFECT + + def _is_optimistic(self, attribute: str) -> bool: """Return True if the attribute is optimistically updated.""" - return getattr(self, f"_optimistic_{attribute}") + attr: bool = getattr(self, f"_optimistic_{attribute}") + return attr - def _prepare_subscribe_topics(self): # noqa: C901 + def _prepare_subscribe_topics(self) -> None: # noqa: C901 """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, dict[str, Any]] = {} - def add_topic(topic, msg_callback): + def add_topic(topic: str, msg_callback: MessageCallbackType) -> None: """Add a topic.""" if self._topic[topic] is not None: topics[topic] = { @@ -425,19 +440,21 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @callback @log_messages(self.hass, self.entity_id) - def state_received(msg): + def state_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE]( + msg.payload, PayloadSentinel.NONE + ) if not payload: _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) return if payload == self._payload["on"]: - self._state = True + self._attr_is_on = True elif payload == self._payload["off"]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: @@ -450,56 +467,64 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @callback @log_messages(self.hass, self.entity_id) - def brightness_received(msg): + def brightness_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for the brightness.""" payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic) return device_value = float(payload) percent_bright = device_value / self._config[CONF_BRIGHTNESS_SCALE] - self._brightness = percent_bright * 255 + self._attr_brightness = min(round(percent_bright * 255), 255) + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) - def _rgbx_received(msg, template, color_mode, convert_color): + def _rgbx_received( + msg: ReceiveMessage, + template: str, + color_mode: ColorMode, + convert_color: Callable[..., tuple[int, ...]], + ) -> tuple[int, ...] | None: """Handle new MQTT messages for RGBW and RGBWW.""" - payload = self._value_templates[template](msg.payload, None) - if not payload: + payload = self._value_templates[template]( + msg.payload, PayloadSentinel.DEFAULT + ) + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug( "Ignoring empty %s message from '%s'", color_mode, msg.topic ) return None - color = tuple(int(val) for val in payload.split(",")) + color = tuple(int(val) for val in str(payload).split(",")) if self._optimistic_color_mode: - self._color_mode = color_mode + self._attr_color_mode = color_mode if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: rgb = convert_color(*color) percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 - self._brightness = percent_bright * 255 + self._attr_brightness = min(round(percent_bright * 255), 255) return color @callback @log_messages(self.hass, self.entity_id) - def rgb_received(msg): + def rgb_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for RGB.""" rgb = _rgbx_received( msg, CONF_RGB_VALUE_TEMPLATE, ColorMode.RGB, lambda *x: x ) - if not rgb: + if rgb is None: return - self._rgb_color = rgb + self._attr_rgb_color = cast(tuple[int, int, int], rgb) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @callback @log_messages(self.hass, self.entity_id) - def rgbw_received(msg): + def rgbw_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for RGBW.""" rgbw = _rgbx_received( msg, @@ -507,16 +532,16 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ColorMode.RGBW, color_util.color_rgbw_to_rgb, ) - if not rgbw: + if rgbw is None: return - self._rgbw_color = rgbw + self._attr_rgbw_color = cast(tuple[int, int, int, int], rgbw) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) @callback @log_messages(self.hass, self.entity_id) - def rgbww_received(msg): + def rgbww_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for RGBWW.""" rgbww = _rgbx_received( msg, @@ -524,76 +549,78 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ColorMode.RGBWW, color_util.color_rgbww_to_rgb, ) - if not rgbww: + if rgbww is None: return - self._rgbww_color = rgbww + self._attr_rgbww_color = cast(tuple[int, int, int, int, int], rgbww) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) @callback @log_messages(self.hass, self.entity_id) - def color_mode_received(msg): + def color_mode_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for color mode.""" payload = self._value_templates[CONF_COLOR_MODE_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty color mode message from '%s'", msg.topic) return - self._color_mode = payload + self._attr_color_mode = str(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) @callback @log_messages(self.hass, self.entity_id) - def color_temp_received(msg): + def color_temp_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for color temperature.""" payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return if self._optimistic_color_mode: - self._color_mode = ColorMode.COLOR_TEMP - self._color_temp = int(payload) + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = int(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) @callback @log_messages(self.hass, self.entity_id) - def effect_received(msg): + def effect_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for effect.""" payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic) return - self._effect = payload + self._attr_effect = str(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) @callback @log_messages(self.hass, self.entity_id) - def hs_received(msg): + def hs_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for hs color.""" - payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload, None) - if not payload: + payload = self._value_templates[CONF_HS_VALUE_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return try: - hs_color = tuple(float(val) for val in payload.split(",", 2)) + hs_color = tuple(float(val) for val in str(payload).split(",", 2)) if self._optimistic_color_mode: - self._color_mode = ColorMode.HS - self._hs_color = hs_color + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = cast(tuple[float, float], hs_color) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) @@ -602,17 +629,19 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @callback @log_messages(self.hass, self.entity_id) - def xy_received(msg): + def xy_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for xy color.""" - payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload, None) - if not payload: + payload = self._value_templates[CONF_XY_VALUE_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return - xy_color = tuple(float(val) for val in payload.split(",")) + xy_color = tuple(float(val) for val in str(payload).split(",", 2)) if self._optimistic_color_mode: - self._color_mode = ColorMode.XY - self._xy_color = xy_color + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = cast(tuple[float, float], xy_color) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_XY_STATE_TOPIC, xy_received) @@ -621,21 +650,23 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) last_state = await self.async_get_last_state() - def restore_state(attribute, condition_attribute=None): + def restore_state( + attribute: str, condition_attribute: str | None = None + ) -> None: """Restore a state attribute.""" if condition_attribute is None: condition_attribute = attribute optimistic = self._is_optimistic(condition_attribute) if optimistic and last_state and last_state.attributes.get(attribute): - setattr(self, f"_{attribute}", last_state.attributes[attribute]) + setattr(self, f"_attr_{attribute}", last_state.attributes[attribute]) if self._topic[CONF_STATE_TOPIC] is None and self._optimistic and last_state: - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON restore_state(ATTR_BRIGHTNESS) restore_state(ATTR_RGB_COLOR) restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) @@ -649,111 +680,32 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - if brightness := self._brightness: - brightness = min(round(brightness), 255) - return brightness - - @property - def color_mode(self): - """Return current color mode.""" - return self._color_mode - - @property - def hs_color(self): - """Return the hs color value.""" - return self._hs_color - - @property - def rgb_color(self): - """Return the rgb color value.""" - return self._rgb_color - - @property - def rgbw_color(self): - """Return the rgbw color value.""" - return self._rgbw_color - - @property - def rgbww_color(self): - """Return the rgbww color value.""" - return self._rgbww_color - - @property - def xy_color(self): - """Return the xy color value.""" - return self._xy_color - - @property - def color_temp(self): - """Return the color temperature in mired.""" - return self._color_temp - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._config.get(CONF_MIN_MIREDS, super().min_mireds) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._config.get(CONF_EFFECT_LIST) - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = 0 - supported_features |= ( - self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None - and LightEntityFeature.EFFECT - ) - return supported_features - - async def async_turn_on(self, **kwargs): # noqa: C901 + async def async_turn_on(self, **kwargs: Any) -> None: # noqa: C901 """Turn the device on. This method is a coroutine. """ should_update = False - on_command_type = self._config[CONF_ON_COMMAND_TYPE] + on_command_type: str = self._config[CONF_ON_COMMAND_TYPE] - async def publish(topic, payload): + async def publish(topic: str, payload: PublishPayloadType) -> None: """Publish an MQTT message.""" await self.async_publish( - self._topic[topic], + str(self._topic[topic]), payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], ) - def scale_rgbx(color, brightness=None): + def scale_rgbx( + color: tuple[int, ...], + brightness: int | None = None, + ) -> tuple[int, ...]: """Scale RGBx for brightness.""" if brightness is None: # If there's a brightness topic set, we don't want to scale the RGBx @@ -761,33 +713,39 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: brightness = 255 else: - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 - ) + brightness = kwargs.get(ATTR_BRIGHTNESS) or self.brightness or 255 return tuple(int(channel * brightness / 255) for channel in color) - def render_rgbx(color, template, color_mode): + def render_rgbx( + color: tuple[int, ...], + template: str, + color_mode: ColorMode, + ) -> PublishPayloadType: """Render RGBx payload.""" - if tpl := self._command_templates[template]: - keys = ["red", "green", "blue"] - if color_mode == ColorMode.RGBW: - keys.append("white") - elif color_mode == ColorMode.RGBWW: - keys.extend(["cold_white", "warm_white"]) - rgb_color_str = tpl(variables=zip(keys, color)) - else: - rgb_color_str = ",".join(str(channel) for channel in color) - return rgb_color_str + rgb_color_str = ",".join(str(channel) for channel in color) + keys = ["red", "green", "blue"] + if color_mode == ColorMode.RGBW: + keys.append("white") + elif color_mode == ColorMode.RGBWW: + keys.extend(["cold_white", "warm_white"]) + variables = dict(zip(keys, color)) + return self._command_templates[template](rgb_color_str, variables) - def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): + def set_optimistic( + attribute: str, + value: Any, + color_mode: ColorMode | None = None, + condition_attribute: str | None = None, + ) -> bool: """Optimistically update a state attribute.""" if condition_attribute is None: condition_attribute = attribute if not self._is_optimistic(condition_attribute): return False if color_mode and self._optimistic_color_mode: - self._color_mode = color_mode - setattr(self, f"_{attribute}", value) + self._attr_color_mode = color_mode + + setattr(self, f"_attr_{attribute}", value) return True if on_command_type == "first": @@ -802,14 +760,15 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): and ATTR_BRIGHTNESS not in kwargs and ATTR_WHITE not in kwargs ): - kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 + kwargs[ATTR_BRIGHTNESS] = self.brightness or 255 - hs_color = kwargs.get(ATTR_HS_COLOR) + hs_color: str | None = kwargs.get(ATTR_HS_COLOR) if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: await publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ColorMode.HS) + rgb: tuple[int, int, int] | None if (rgb := kwargs.get(ATTR_RGB_COLOR)) and self._topic[ CONF_RGB_COMMAND_TOPIC ] is not None: @@ -818,6 +777,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_RGB_COLOR, rgb, ColorMode.RGB) + rgbw: tuple[int, int, int, int] | None if (rgbw := kwargs.get(ATTR_RGBW_COLOR)) and self._topic[ CONF_RGBW_COMMAND_TOPIC ] is not None: @@ -826,6 +786,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): await publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) should_update |= set_optimistic(ATTR_RGBW_COLOR, rgbw, ColorMode.RGBW) + rgbww: tuple[int, int, int, int, int] | None if (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) and self._topic[ CONF_RGBWW_COMMAND_TOPIC ] is not None: @@ -834,6 +795,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): await publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) should_update |= set_optimistic(ATTR_RGBWW_COLOR, rgbww, ColorMode.RGBWW) + xy_color: tuple[float, float] | None if (xy_color := kwargs.get(ATTR_XY_COLOR)) and self._topic[ CONF_XY_COMMAND_TOPIC ] is not None: @@ -844,25 +806,25 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ATTR_BRIGHTNESS in kwargs and self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None ): - brightness_normalized = kwargs[ATTR_BRIGHTNESS] / 255 - brightness_scale = self._config[CONF_BRIGHTNESS_SCALE] + brightness_normalized: float = kwargs[ATTR_BRIGHTNESS] / 255 + brightness_scale: int = self._config[CONF_BRIGHTNESS_SCALE] device_brightness = min( round(brightness_normalized * brightness_scale), brightness_scale ) # Make sure the brightness is not rounded down to 0 device_brightness = max(device_brightness, 1) - if tpl := self._command_templates[CONF_BRIGHTNESS_COMMAND_TEMPLATE]: - device_brightness = tpl(variables={"value": device_brightness}) - await publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) + command_tpl = self._command_templates[CONF_BRIGHTNESS_COMMAND_TEMPLATE] + device_brightness_payload = command_tpl(device_brightness, None) + await publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness_payload) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs and ATTR_RGB_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None ): - rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 - rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) + rgb_color = self.rgb_color or (255,) * 3 + rgb_scaled = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) + rgb_s = render_rgbx(rgb_scaled, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( @@ -870,11 +832,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): and ATTR_RGBW_COLOR not in kwargs and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None ): - rgbw_color = ( - self._rgbw_color if self._rgbw_color is not None else (255,) * 4 - ) - rgbw = scale_rgbx(rgbw_color, kwargs[ATTR_BRIGHTNESS]) - rgbw_s = render_rgbx(rgbw, CONF_RGBW_COMMAND_TEMPLATE, ColorMode.RGBW) + rgbw_color = self.rgbw_color or (255,) * 4 + rgbw_b = scale_rgbx(rgbw_color, kwargs[ATTR_BRIGHTNESS]) + rgbw_s = render_rgbx(rgbw_b, CONF_RGBW_COMMAND_TEMPLATE, ColorMode.RGBW) await publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( @@ -882,37 +842,36 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): and ATTR_RGBWW_COLOR not in kwargs and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None ): - rgbww_color = ( - self._rgbww_color if self._rgbww_color is not None else (255,) * 5 - ) - rgbww = scale_rgbx(rgbww_color, kwargs[ATTR_BRIGHTNESS]) - rgbww_s = render_rgbx(rgbww, CONF_RGBWW_COMMAND_TEMPLATE, ColorMode.RGBWW) + rgbww_color = self.rgbww_color or (255,) * 5 + rgbww_b = scale_rgbx(rgbww_color, kwargs[ATTR_BRIGHTNESS]) + rgbww_s = render_rgbx(rgbww_b, CONF_RGBWW_COMMAND_TEMPLATE, ColorMode.RGBWW) await publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) if ( ATTR_COLOR_TEMP in kwargs and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None ): - color_temp = int(kwargs[ATTR_COLOR_TEMP]) - if tpl := self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE]: - color_temp = tpl(variables={"value": color_temp}) - + ct_command_tpl = self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE] + color_temp = ct_command_tpl(int(kwargs[ATTR_COLOR_TEMP]), None) await publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) should_update |= set_optimistic( ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP], ColorMode.COLOR_TEMP ) - if ATTR_EFFECT in kwargs and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: - effect = kwargs[ATTR_EFFECT] - if effect in self._config.get(CONF_EFFECT_LIST): - if tpl := self._command_templates[CONF_EFFECT_COMMAND_TEMPLATE]: - effect = tpl(variables={"value": effect}) + if ( + ATTR_EFFECT in kwargs + and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None + and CONF_EFFECT_LIST in self._config + ): + if kwargs[ATTR_EFFECT] in self._config[CONF_EFFECT_LIST]: + eff_command_tpl = self._command_templates[CONF_EFFECT_COMMAND_TEMPLATE] + effect = eff_command_tpl(kwargs[ATTR_EFFECT], None) await publish(CONF_EFFECT_COMMAND_TOPIC, effect) should_update |= set_optimistic(ATTR_EFFECT, kwargs[ATTR_EFFECT]) if ATTR_WHITE in kwargs and self._topic[CONF_WHITE_COMMAND_TOPIC] is not None: percent_white = float(kwargs[ATTR_WHITE]) / 255 - white_scale = self._config[CONF_WHITE_SCALE] + white_scale: int = self._config[CONF_WHITE_SCALE] device_white_value = min(round(percent_white * white_scale), white_scale) await publish(CONF_WHITE_COMMAND_TOPIC, device_white_value) should_update |= set_optimistic( @@ -927,19 +886,19 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: # Optimistically assume that the light has changed state. - self._state = True + self._attr_is_on = True should_update = True if should_update: self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off. This method is a coroutine. """ await self.async_publish( - self._topic[CONF_COMMAND_TOPIC], + str(self._topic[CONF_COMMAND_TOPIC]), self._payload["off"], self._config[CONF_QOS], self._config[CONF_RETAIN], @@ -948,5 +907,5 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: # Optimistically assume that the light has changed state. - self._state = False + self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index a4a76673176..09413e1f0ac 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -1,6 +1,9 @@ """Support for MQTT JSON lights.""" +from __future__ import annotations + from contextlib import suppress import logging +from typing import Any, cast import voluptuous as vol @@ -29,6 +32,7 @@ from homeassistant.components.light import ( filter_supported_color_modes, valid_supported_color_modes, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, @@ -40,11 +44,12 @@ from homeassistant.const import ( CONF_XY, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import json_dumps, json_loads from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util from .. import subscription @@ -58,6 +63,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..models import ReceiveMessage from ..util import get_mqtt_data, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( @@ -98,7 +104,7 @@ CONF_MIN_MIREDS = "min_mireds" CONF_WHITE_VALUE = "white_value" -def valid_color_configuration(config): +def valid_color_configuration(config: ConfigType) -> ConfigType: """Test color_mode is not combined with deprecated config.""" deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_XY} if config[CONF_COLOR_MODE] and any(config.get(key) for key in deprecated): @@ -152,7 +158,7 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) -# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Lights under the light platform key was deprecated in HA Core 2022.6 PLATFORM_SCHEMA_JSON = vol.All( cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), valid_color_configuration, @@ -174,8 +180,12 @@ PLATFORM_SCHEMA_MODERN_JSON = vol.All( async def async_setup_entity_json( - hass, config: ConfigType, async_add_entities, config_entry, discovery_data -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, +) -> None: """Set up a MQTT JSON Light.""" async_add_entities([MqttLightJson(hass, config, config_entry, discovery_data)]) @@ -186,38 +196,36 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _flash_times: dict[str, int | None] + _topic: dict[str, str | None] + _optimistic: bool + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize MQTT JSON light.""" - self._state = None - self._supported_features = 0 - - self._topic = None - self._optimistic = False - self._brightness = None - self._color_mode = None - self._color_temp = None - self._effect = None - self._fixed_color_mode = None - self._flash_times = None - self._hs = None - self._rgb = None - self._rgbw = None - self._rgbww = None - self._xy = None - + self._fixed_color_mode: ColorMode | str | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA_JSON - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_max_mireds = config.get(CONF_MAX_MIREDS, super().max_mireds) + self._attr_min_mireds = config.get(CONF_MIN_MIREDS, super().min_mireds) + self._attr_effect_list = config.get(CONF_EFFECT_LIST) + self._topic = { key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC) } - optimistic = config[CONF_OPTIMISTIC] + optimistic: bool = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None self._flash_times = { @@ -225,10 +233,12 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): for key in (CONF_FLASH_TIME_SHORT, CONF_FLASH_TIME_LONG) } - self._supported_features = ( + self._attr_supported_features = ( LightEntityFeature.TRANSITION | LightEntityFeature.FLASH ) - self._supported_features |= config[CONF_EFFECT] and LightEntityFeature.EFFECT + self._attr_supported_features |= ( + config[CONF_EFFECT] and LightEntityFeature.EFFECT + ) if not self._config[CONF_COLOR_MODE]: color_modes = {ColorMode.ONOFF} if config[CONF_BRIGHTNESS]: @@ -237,22 +247,22 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): color_modes.add(ColorMode.COLOR_TEMP) if config[CONF_HS] or config[CONF_RGB] or config[CONF_XY]: color_modes.add(ColorMode.HS) - self._supported_color_modes = filter_supported_color_modes(color_modes) - if len(self._supported_color_modes) == 1: - self._fixed_color_mode = next(iter(self._supported_color_modes)) + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) + if self.supported_color_modes and len(self.supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self.supported_color_modes)) else: - self._supported_color_modes = self._config[CONF_SUPPORTED_COLOR_MODES] - if len(self._supported_color_modes) == 1: - self._color_mode = next(iter(self._supported_color_modes)) + self._attr_supported_color_modes = self._config[CONF_SUPPORTED_COLOR_MODES] + if self.supported_color_modes and len(self.supported_color_modes) == 1: + self._attr_color_mode = next(iter(self.supported_color_modes)) - def _update_color(self, values): + def _update_color(self, values: dict[str, Any]) -> None: if not self._config[CONF_COLOR_MODE]: # Deprecated color handling 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) + self._attr_hs_color = color_util.color_RGB_to_hs(red, green, blue) except KeyError: pass except ValueError: @@ -264,7 +274,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): 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) + self._attr_hs_color = color_util.color_xy_to_hs(x_color, y_color) except KeyError: pass except ValueError: @@ -276,7 +286,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): try: hue = float(values["color"]["h"]) saturation = float(values["color"]["s"]) - self._hs = (hue, saturation) + self._attr_hs_color = (hue, saturation) except KeyError: pass except ValueError: @@ -285,7 +295,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): ) return else: - color_mode = values["color_mode"] + color_mode: str = values["color_mode"] if not self._supports_color_mode(color_mode): _LOGGER.warning( "Invalid color mode received for entity %s", self.entity_id @@ -293,80 +303,80 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): return try: if color_mode == ColorMode.COLOR_TEMP: - self._color_temp = int(values["color_temp"]) - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = int(values["color_temp"]) + self._attr_color_mode = ColorMode.COLOR_TEMP elif color_mode == ColorMode.HS: hue = float(values["color"]["h"]) saturation = float(values["color"]["s"]) - self._color_mode = ColorMode.HS - self._hs = (hue, saturation) + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = (hue, saturation) elif color_mode == ColorMode.RGB: r = int(values["color"]["r"]) # pylint: disable=invalid-name g = int(values["color"]["g"]) # pylint: disable=invalid-name b = int(values["color"]["b"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.RGB - self._rgb = (r, g, b) + self._attr_color_mode = ColorMode.RGB + self._attr_rgb_color = (r, g, b) elif color_mode == ColorMode.RGBW: r = int(values["color"]["r"]) # pylint: disable=invalid-name g = int(values["color"]["g"]) # pylint: disable=invalid-name b = int(values["color"]["b"]) # pylint: disable=invalid-name w = int(values["color"]["w"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.RGBW - self._rgbw = (r, g, b, w) + self._attr_color_mode = ColorMode.RGBW + self._attr_rgbw_color = (r, g, b, w) elif color_mode == ColorMode.RGBWW: r = int(values["color"]["r"]) # pylint: disable=invalid-name g = int(values["color"]["g"]) # pylint: disable=invalid-name b = int(values["color"]["b"]) # pylint: disable=invalid-name c = int(values["color"]["c"]) # pylint: disable=invalid-name w = int(values["color"]["w"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.RGBWW - self._rgbww = (r, g, b, c, w) + self._attr_color_mode = ColorMode.RGBWW + self._attr_rgbww_color = (r, g, b, c, w) elif color_mode == ColorMode.WHITE: - self._color_mode = ColorMode.WHITE + self._attr_color_mode = ColorMode.WHITE elif color_mode == ColorMode.XY: x = float(values["color"]["x"]) # pylint: disable=invalid-name y = float(values["color"]["y"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.XY - self._xy = (x, y) + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = (x, y) except (KeyError, ValueError): _LOGGER.warning( "Invalid or incomplete color value received for entity %s", self.entity_id, ) - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def state_received(msg): + def state_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - values = json_loads(msg.payload) + values: dict[str, Any] = json_loads(msg.payload) if values["state"] == "ON": - self._state = True + self._attr_is_on = True elif values["state"] == "OFF": - self._state = False + self._attr_is_on = False elif values["state"] is None: - self._state = None + self._attr_is_on = None if ( not self._config[CONF_COLOR_MODE] - and color_supported(self._supported_color_modes) + and color_supported(self.supported_color_modes) and "color" in values ): # Deprecated color handling if values["color"] is None: - self._hs = None + self._attr_hs_color = None else: self._update_color(values) if self._config[CONF_COLOR_MODE] and "color_mode" in values: self._update_color(values) - if brightness_supported(self._supported_color_modes): + if brightness_supported(self.supported_color_modes): try: - self._brightness = int( + self._attr_brightness = int( values["brightness"] / float(self._config[CONF_BRIGHTNESS_SCALE]) * 255 @@ -380,15 +390,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): ) if ( - ColorMode.COLOR_TEMP in self._supported_color_modes + self.supported_color_modes + and ColorMode.COLOR_TEMP in self.supported_color_modes and not self._config[CONF_COLOR_MODE] ): # Deprecated color handling try: if values["color_temp"] is None: - self._color_temp = None + self._attr_color_temp = None else: - self._color_temp = int(values["color_temp"]) + self._attr_color_temp = int(values["color_temp"]) except KeyError: pass except ValueError: @@ -397,9 +408,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self.entity_id, ) - if self._supported_features and LightEntityFeature.EFFECT: + if self.supported_features and LightEntityFeature.EFFECT: with suppress(KeyError): - self._effect = values["effect"] + self._attr_effect = values["effect"] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -417,147 +428,95 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) last_state = await self.async_get_last_state() if self._optimistic and last_state: - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON last_attributes = last_state.attributes - self._brightness = last_attributes.get(ATTR_BRIGHTNESS, self._brightness) - self._color_mode = last_attributes.get(ATTR_COLOR_MODE, self._color_mode) - self._color_temp = last_attributes.get(ATTR_COLOR_TEMP, self._color_temp) - self._effect = last_attributes.get(ATTR_EFFECT, self._effect) - self._hs = last_attributes.get(ATTR_HS_COLOR, self._hs) - self._rgb = last_attributes.get(ATTR_RGB_COLOR, self._rgb) - self._rgbw = last_attributes.get(ATTR_RGBW_COLOR, self._rgbw) - self._rgbww = last_attributes.get(ATTR_RGBWW_COLOR, self._rgbww) - self._xy = last_attributes.get(ATTR_XY_COLOR, self._xy) + self._attr_brightness = last_attributes.get( + ATTR_BRIGHTNESS, self.brightness + ) + self._attr_color_mode = last_attributes.get( + ATTR_COLOR_MODE, self.color_mode + ) + self._attr_color_temp = last_attributes.get( + ATTR_COLOR_TEMP, self.color_temp + ) + self._attr_effect = last_attributes.get(ATTR_EFFECT, self.effect) + self._attr_hs_color = last_attributes.get(ATTR_HS_COLOR, self.hs_color) + self._attr_rgb_color = last_attributes.get(ATTR_RGB_COLOR, self.rgb_color) + self._attr_rgbw_color = last_attributes.get( + ATTR_RGBW_COLOR, self.rgbw_color + ) + self._attr_rgbww_color = last_attributes.get( + ATTR_RGBWW_COLOR, self.rgbww_color + ) + self._attr_xy_color = last_attributes.get(ATTR_XY_COLOR, self.xy_color) @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def color_temp(self): - """Return the color temperature in mired.""" - return self._color_temp - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._config.get(CONF_MIN_MIREDS, super().min_mireds) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._config.get(CONF_EFFECT_LIST) - - @property - def hs_color(self): - """Return the hs color value.""" - return self._hs - - @property - def rgb_color(self): - """Return the hs color value.""" - return self._rgb - - @property - def rgbw_color(self): - """Return the hs color value.""" - return self._rgbw - - @property - def rgbww_color(self): - """Return the hs color value.""" - return self._rgbww - - @property - def xy_color(self): - """Return the hs color value.""" - return self._xy - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def color_mode(self): + def color_mode(self) -> ColorMode | str | None: """Return current color mode.""" if self._config[CONF_COLOR_MODE]: - return self._color_mode + return self._attr_color_mode if self._fixed_color_mode: # Legacy light with support for a single color mode return self._fixed_color_mode # Legacy light with support for ct + hs, prioritize hs - if self._hs is not None: + if self.hs_color is not None: return ColorMode.HS return ColorMode.COLOR_TEMP - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - - def _set_flash_and_transition(self, message, **kwargs): + def _set_flash_and_transition(self, message: dict[str, Any], **kwargs: Any) -> None: if ATTR_TRANSITION in kwargs: message["transition"] = kwargs[ATTR_TRANSITION] if ATTR_FLASH in kwargs: - flash = kwargs.get(ATTR_FLASH) + flash: str = kwargs[ATTR_FLASH] if flash == FLASH_LONG: message["flash"] = self._flash_times[CONF_FLASH_TIME_LONG] elif flash == FLASH_SHORT: message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT] - def _scale_rgbxx(self, rgbxx, kwargs): + def _scale_rgbxx(self, rgbxx: tuple[int, ...], kwargs: Any) -> tuple[int, ...]: # If there's a brightness topic set, we don't want to scale the # RGBxx values given using the brightness. + brightness: int if self._config[CONF_BRIGHTNESS]: brightness = 255 else: brightness = kwargs.get(ATTR_BRIGHTNESS, 255) return tuple(round(i / 255 * brightness) for i in rgbxx) - def _supports_color_mode(self, color_mode): + def _supports_color_mode(self, color_mode: ColorMode | str) -> bool: """Return True if the light natively supports a color mode.""" return ( - self._config[CONF_COLOR_MODE] and color_mode in self.supported_color_modes + self._config[CONF_COLOR_MODE] + and self.supported_color_modes is not None + and color_mode in self.supported_color_modes ) - async def async_turn_on(self, **kwargs): # noqa: C901 + async def async_turn_on(self, **kwargs: Any) -> None: # noqa: C901 """Turn the device on. This method is a coroutine. """ + brightness: int should_update = False - - message = {"state": "ON"} + hs_color: tuple[float, float] + message: dict[str, Any] = {"state": "ON"} + rgb: tuple[int, ...] + rgbw: tuple[int, ...] + rgbcw: tuple[int, ...] + xy_color: tuple[float, float] if ATTR_HS_COLOR in kwargs and ( self._config[CONF_HS] or self._config[CONF_RGB] or self._config[CONF_XY] @@ -587,54 +546,54 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): message["color"]["s"] = hs_color[1] if self._optimistic: - self._color_temp = None - self._hs = kwargs[ATTR_HS_COLOR] + self._attr_color_temp = None + self._attr_hs_color = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_HS_COLOR in kwargs and self._supports_color_mode(ColorMode.HS): hs_color = kwargs[ATTR_HS_COLOR] message["color"] = {"h": hs_color[0], "s": hs_color[1]} if self._optimistic: - self._color_mode = ColorMode.HS - self._hs = hs_color + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color should_update = True if ATTR_RGB_COLOR in kwargs and self._supports_color_mode(ColorMode.RGB): rgb = self._scale_rgbxx(kwargs[ATTR_RGB_COLOR], kwargs) message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2]} if self._optimistic: - self._color_mode = ColorMode.RGB - self._rgb = rgb + self._attr_color_mode = ColorMode.RGB + self._attr_rgb_color = cast(tuple[int, int, int], rgb) should_update = True if ATTR_RGBW_COLOR in kwargs and self._supports_color_mode(ColorMode.RGBW): - rgb = self._scale_rgbxx(kwargs[ATTR_RGBW_COLOR], kwargs) - message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2], "w": rgb[3]} + rgbw = self._scale_rgbxx(kwargs[ATTR_RGBW_COLOR], kwargs) + message["color"] = {"r": rgbw[0], "g": rgbw[1], "b": rgbw[2], "w": rgbw[3]} if self._optimistic: - self._color_mode = ColorMode.RGBW - self._rgbw = rgb + self._attr_color_mode = ColorMode.RGBW + self._attr_rgbw_color = cast(tuple[int, int, int, int], rgbw) should_update = True if ATTR_RGBWW_COLOR in kwargs and self._supports_color_mode(ColorMode.RGBWW): - rgb = self._scale_rgbxx(kwargs[ATTR_RGBWW_COLOR], kwargs) + rgbcw = self._scale_rgbxx(kwargs[ATTR_RGBWW_COLOR], kwargs) message["color"] = { - "r": rgb[0], - "g": rgb[1], - "b": rgb[2], - "c": rgb[3], - "w": rgb[4], + "r": rgbcw[0], + "g": rgbcw[1], + "b": rgbcw[2], + "c": rgbcw[3], + "w": rgbcw[4], } if self._optimistic: - self._color_mode = ColorMode.RGBWW - self._rgbww = rgb + self._attr_color_mode = ColorMode.RGBWW + self._attr_rgbww_color = cast(tuple[int, int, int, int, int], rgbcw) should_update = True if ATTR_XY_COLOR in kwargs and self._supports_color_mode(ColorMode.XY): - xy = kwargs[ATTR_XY_COLOR] # pylint: disable=invalid-name - message["color"] = {"x": xy[0], "y": xy[1]} + xy_color = kwargs[ATTR_XY_COLOR] + message["color"] = {"x": xy_color[0], "y": xy_color[1]} if self._optimistic: - self._color_mode = ColorMode.XY - self._xy = xy + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy_color should_update = True self._set_flash_and_transition(message, **kwargs) @@ -650,23 +609,23 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): message["brightness"] = device_brightness if self._optimistic: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] should_update = True if ATTR_COLOR_TEMP in kwargs: message["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if self._optimistic: - self._color_mode = ColorMode.COLOR_TEMP - self._color_temp = kwargs[ATTR_COLOR_TEMP] - self._hs = None + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = kwargs[ATTR_COLOR_TEMP] + self._attr_hs_color = None should_update = True if ATTR_EFFECT in kwargs: message["effect"] = kwargs[ATTR_EFFECT] if self._optimistic: - self._effect = kwargs[ATTR_EFFECT] + self._attr_effect = kwargs[ATTR_EFFECT] should_update = True if ATTR_WHITE in kwargs and self._supports_color_mode(ColorMode.WHITE): @@ -678,12 +637,12 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): message["white"] = device_white_level if self._optimistic: - self._color_mode = ColorMode.WHITE - self._brightness = kwargs[ATTR_WHITE] + self._attr_color_mode = ColorMode.WHITE + self._attr_brightness = kwargs[ATTR_WHITE] should_update = True await self.async_publish( - self._topic[CONF_COMMAND_TOPIC], + str(self._topic[CONF_COMMAND_TOPIC]), json_dumps(message), self._config[CONF_QOS], self._config[CONF_RETAIN], @@ -692,23 +651,23 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: # Optimistically assume that the light has changed state. - self._state = True + self._attr_is_on = True should_update = True if should_update: self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off. This method is a coroutine. """ - message = {"state": "OFF"} + message: dict[str, Any] = {"state": "OFF"} self._set_flash_and_transition(message, **kwargs) await self.async_publish( - self._topic[CONF_COMMAND_TOPIC], + str(self._topic[CONF_COMMAND_TOPIC]), json_dumps(message), self._config[CONF_QOS], self._config[CONF_RETAIN], @@ -717,5 +676,5 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: # Optimistically assume that the light has changed state. - self._state = False + self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 33c7f1cea1b..21691acc916 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -1,5 +1,9 @@ """Support for MQTT Template lights.""" +from __future__ import annotations + +from collections.abc import Callable import logging +from typing import Any import voluptuous as vol @@ -16,6 +20,7 @@ from homeassistant.components.light import ( LightEntityFeature, filter_supported_color_modes, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, @@ -23,9 +28,11 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType import homeassistant.util.color as color_util from .. import subscription @@ -40,7 +47,13 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity -from ..models import MqttValueTemplate +from ..models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from ..util import get_mqtt_data from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -65,6 +78,17 @@ CONF_MIN_MIREDS = "min_mireds" CONF_RED_TEMPLATE = "red_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" +COMMAND_TEMPLATES = (CONF_COMMAND_ON_TEMPLATE, CONF_COMMAND_OFF_TEMPLATE) +VALUE_TEMPLATES = ( + CONF_BLUE_TEMPLATE, + CONF_BRIGHTNESS_TEMPLATE, + CONF_COLOR_TEMP_TEMPLATE, + CONF_EFFECT_TEMPLATE, + CONF_GREEN_TEMPLATE, + CONF_RED_TEMPLATE, + CONF_STATE_TEMPLATE, +) + _PLATFORM_SCHEMA_BASE = ( MQTT_RW_SCHEMA.extend( { @@ -88,7 +112,7 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) -# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Lights under the light platform key was deprecated in HA Core 2022.6 PLATFORM_SCHEMA_TEMPLATE = vol.All( cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) @@ -107,8 +131,12 @@ PLATFORM_SCHEMA_MODERN_TEMPLATE = vol.All( async def async_setup_entity_template( - hass, config, async_add_entities, config_entry, discovery_data -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, +) -> None: """Set up a MQTT Template light.""" async_add_entities([MqttLightTemplate(hass, config, config_entry, discovery_data)]) @@ -118,142 +146,145 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED + _optimistic: bool + _command_templates: dict[ + str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType] + ] + _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _fixed_color_mode: ColorMode | str | None + _topics: dict[str, str | None] - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize a MQTT Template light.""" - self._state = None - - self._topics = None - self._templates = None - self._optimistic = False - - # features - self._brightness = None - self._fixed_color_mode = None - self._color_temp = None - self._hs = None - self._effect = None - MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA_TEMPLATE - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_max_mireds = config.get(CONF_MAX_MIREDS, super().max_mireds) + self._attr_min_mireds = config.get(CONF_MIN_MIREDS, super().min_mireds) + self._attr_effect_list = config.get(CONF_EFFECT_LIST) + self._topics = { key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC) } - self._templates = { - key: config.get(key) - for key in ( - CONF_BLUE_TEMPLATE, - CONF_BRIGHTNESS_TEMPLATE, - CONF_COLOR_TEMP_TEMPLATE, - CONF_COMMAND_OFF_TEMPLATE, - CONF_COMMAND_ON_TEMPLATE, - CONF_EFFECT_TEMPLATE, - CONF_GREEN_TEMPLATE, - CONF_RED_TEMPLATE, - CONF_STATE_TEMPLATE, - ) + self._command_templates = { + key: MqttCommandTemplate(config[key], entity=self).async_render + for key in COMMAND_TEMPLATES } - optimistic = config[CONF_OPTIMISTIC] + self._value_templates = { + key: MqttValueTemplate( + config.get(key), entity=self + ).async_render_with_possible_json_value + for key in VALUE_TEMPLATES + } + optimistic: bool = config[CONF_OPTIMISTIC] self._optimistic = ( optimistic or self._topics[CONF_STATE_TOPIC] is None - or self._templates[CONF_STATE_TEMPLATE] is None + or CONF_STATE_TEMPLATE not in self._config ) color_modes = {ColorMode.ONOFF} - if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + if CONF_BRIGHTNESS_TEMPLATE in config: color_modes.add(ColorMode.BRIGHTNESS) - if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: + if CONF_COLOR_TEMP_TEMPLATE in config: color_modes.add(ColorMode.COLOR_TEMP) if ( - self._templates[CONF_RED_TEMPLATE] is not None - and self._templates[CONF_GREEN_TEMPLATE] is not None - and self._templates[CONF_BLUE_TEMPLATE] is not None + CONF_RED_TEMPLATE in config + and CONF_GREEN_TEMPLATE in config + and CONF_BLUE_TEMPLATE in config ): color_modes.add(ColorMode.HS) - self._supported_color_modes = filter_supported_color_modes(color_modes) - if len(self._supported_color_modes) == 1: - self._fixed_color_mode = next(iter(self._supported_color_modes)) + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) + self._fixed_color_mode = None + if self.supported_color_modes and len(self.supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self.supported_color_modes)) + self._attr_color_mode = self._fixed_color_mode - def _prepare_subscribe_topics(self): + features = LightEntityFeature.FLASH | LightEntityFeature.TRANSITION + if config.get(CONF_EFFECT_LIST) is not None: + features = features | LightEntityFeature.EFFECT + self._attr_supported_features = features + + def _update_color_mode(self) -> None: + """Update the color_mode attribute.""" + if self._fixed_color_mode: + return + # Support for ct + hs, prioritize hs + self._attr_color_mode = ColorMode.HS if self.hs_color else ColorMode.COLOR_TEMP + + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - for tpl in self._templates.values(): - if tpl is not None: - tpl = MqttValueTemplate(tpl, entity=self) @callback @log_messages(self.hass, self.entity_id) - def state_received(msg): + def state_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - state = self._templates[ - CONF_STATE_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) + state = self._value_templates[CONF_STATE_TEMPLATE](msg.payload) if state == STATE_ON: - self._state = True + self._attr_is_on = True elif state == STATE_OFF: - self._state = False + self._attr_is_on = False elif state == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None else: _LOGGER.warning("Invalid state value received") - if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + if CONF_BRIGHTNESS_TEMPLATE in self._config: try: - self._brightness = int( - self._templates[ - CONF_BRIGHTNESS_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) + self._attr_brightness = int( + self._value_templates[CONF_BRIGHTNESS_TEMPLATE](msg.payload) ) except ValueError: _LOGGER.warning("Invalid brightness value received") - if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: + if CONF_COLOR_TEMP_TEMPLATE in self._config: try: - color_temp = self._templates[ - CONF_COLOR_TEMP_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - self._color_temp = int(color_temp) if color_temp != "None" else None + color_temp = self._value_templates[CONF_COLOR_TEMP_TEMPLATE]( + msg.payload + ) + self._attr_color_temp = ( + int(color_temp) if color_temp != "None" else None + ) except ValueError: _LOGGER.warning("Invalid color temperature value received") if ( - self._templates[CONF_RED_TEMPLATE] is not None - and self._templates[CONF_GREEN_TEMPLATE] is not None - and self._templates[CONF_BLUE_TEMPLATE] is not None + CONF_RED_TEMPLATE in self._config + and CONF_GREEN_TEMPLATE in self._config + and CONF_BLUE_TEMPLATE in self._config ): try: - red = self._templates[ - CONF_RED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - green = self._templates[ - CONF_GREEN_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - blue = self._templates[ - CONF_BLUE_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) + red = self._value_templates[CONF_RED_TEMPLATE](msg.payload) + green = self._value_templates[CONF_GREEN_TEMPLATE](msg.payload) + blue = self._value_templates[CONF_BLUE_TEMPLATE](msg.payload) if red == "None" and green == "None" and blue == "None": - self._hs = None + self._attr_hs_color = None else: - self._hs = color_util.color_RGB_to_hs( + self._attr_hs_color = color_util.color_RGB_to_hs( int(red), int(green), int(blue) ) + self._update_color_mode() except ValueError: _LOGGER.warning("Invalid color value received") - if self._templates[CONF_EFFECT_TEMPLATE] is not None: - effect = self._templates[ - CONF_EFFECT_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - - if effect in self._config.get(CONF_EFFECT_LIST): - self._effect = effect + if CONF_EFFECT_TEMPLATE in self._config: + effect = str(self._value_templates[CONF_EFFECT_TEMPLATE](msg.payload)) + if ( + effect_list := self._config[CONF_EFFECT_LIST] + ) and effect in effect_list: + self._attr_effect = effect else: _LOGGER.warning("Unsupported effect value received") @@ -273,100 +304,62 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) last_state = await self.async_get_last_state() if self._optimistic and last_state: - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON if last_state.attributes.get(ATTR_BRIGHTNESS): - self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + self._attr_brightness = last_state.attributes.get(ATTR_BRIGHTNESS) if last_state.attributes.get(ATTR_HS_COLOR): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + self._attr_hs_color = last_state.attributes.get(ATTR_HS_COLOR) + self._update_color_mode() if last_state.attributes.get(ATTR_COLOR_TEMP): - self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + self._attr_color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) if last_state.attributes.get(ATTR_EFFECT): - self._effect = last_state.attributes.get(ATTR_EFFECT) + self._attr_effect = last_state.attributes.get(ATTR_EFFECT) @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def color_temp(self): - """Return the color temperature in mired.""" - return self._color_temp - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._config.get(CONF_MIN_MIREDS, super().min_mireds) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - - @property - def hs_color(self): - """Return the hs color value [int, int].""" - return self._hs - - @property - def is_on(self): - """Return True if entity is on.""" - return self._state - - @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" return self._optimistic - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._config.get(CONF_EFFECT_LIST) - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on. This method is a coroutine. """ - values = {"state": True} + values: dict[str, Any] = {"state": True} if self._optimistic: - self._state = True + self._attr_is_on = True if ATTR_BRIGHTNESS in kwargs: values["brightness"] = int(kwargs[ATTR_BRIGHTNESS]) if self._optimistic: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] if ATTR_COLOR_TEMP in kwargs: values["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if self._optimistic: - self._color_temp = kwargs[ATTR_COLOR_TEMP] - self._hs = None + self._attr_color_temp = kwargs[ATTR_COLOR_TEMP] + self._attr_hs_color = None + self._update_color_mode() if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] # If there's a brightness topic set, we don't want to scale the RGB # values given using the brightness. - if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + if CONF_BRIGHTNESS_TEMPLATE in self._config: brightness = 255 else: brightness = kwargs.get( ATTR_BRIGHTNESS, - self._brightness if self._brightness is not None else 255, + self._attr_brightness if self._attr_brightness is not None else 255, ) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100 @@ -378,14 +371,15 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): values["sat"] = hs_color[1] if self._optimistic: - self._color_temp = None - self._hs = kwargs[ATTR_HS_COLOR] + self._attr_color_temp = None + self._attr_hs_color = kwargs[ATTR_HS_COLOR] + self._update_color_mode() if ATTR_EFFECT in kwargs: values["effect"] = kwargs.get(ATTR_EFFECT) if self._optimistic: - self._effect = kwargs[ATTR_EFFECT] + self._attr_effect = kwargs[ATTR_EFFECT] if ATTR_FLASH in kwargs: values["flash"] = kwargs.get(ATTR_FLASH) @@ -394,10 +388,8 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): values["transition"] = kwargs[ATTR_TRANSITION] await self.async_publish( - self._topics[CONF_COMMAND_TOPIC], - self._templates[CONF_COMMAND_ON_TEMPLATE].async_render( - parse_result=False, **values - ), + str(self._topics[CONF_COMMAND_TOPIC]), + self._command_templates[CONF_COMMAND_ON_TEMPLATE](None, values), self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], @@ -406,23 +398,21 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off. This method is a coroutine. """ - values = {"state": False} + values: dict[str, Any] = {"state": False} if self._optimistic: - self._state = False + self._attr_is_on = False if ATTR_TRANSITION in kwargs: values["transition"] = kwargs[ATTR_TRANSITION] await self.async_publish( - self._topics[CONF_COMMAND_TOPIC], - self._templates[CONF_COMMAND_OFF_TEMPLATE].async_render( - parse_result=False, **values - ), + str(self._topics[CONF_COMMAND_TOPIC]), + self._command_templates[CONF_COMMAND_OFF_TEMPLATE](None, values), self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], @@ -430,27 +420,3 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: self.async_write_ha_state() - - @property - def color_mode(self): - """Return current color mode.""" - if self._fixed_color_mode: - return self._fixed_color_mode - # Support for ct + hs, prioritize hs - if self._hs is not None: - return ColorMode.HS - return ColorMode.COLOR_TEMP - - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - features = LightEntityFeature.FLASH | LightEntityFeature.TRANSITION - if self._config.get(CONF_EFFECT_LIST) is not None: - features = features | LightEntityFeature.EFFECT - - return features diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index c9bdd696896..b956f2e1b88 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,6 +1,7 @@ """Support for MQTT locks.""" from __future__ import annotations +from collections.abc import Callable import functools from typing import Any @@ -29,10 +30,9 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType from .util import get_mqtt_data CONF_PAYLOAD_LOCK = "payload_lock" @@ -70,32 +70,15 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Locks under the lock platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Locks under the lock platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(lock.DOMAIN), ) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT locks configured under the lock platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - lock.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -112,8 +95,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Lock platform.""" async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) @@ -125,39 +108,50 @@ class MqttLock(MqttEntity, LockEntity): _entity_id_format = lock.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): - """Initialize the lock.""" - self._state = False - self._optimistic = False + _optimistic: bool + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the lock.""" + self._attr_is_locked = False MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._optimistic = config[CONF_OPTIMISTIC] self._value_template = MqttValueTemplate( - self._config.get(CONF_VALUE_TEMPLATE), + config.get(CONF_VALUE_TEMPLATE), entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + self._attr_supported_features = LockEntityFeature(0) + if CONF_PAYLOAD_OPEN in config: + self._attr_supported_features |= LockEntityFeature.OPEN + + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" payload = self._value_template(msg.payload) if payload == self._config[CONF_STATE_LOCKED]: - self._state = True + self._attr_is_locked = True elif payload == self._config[CONF_STATE_UNLOCKED]: - self._state = False + self._attr_is_locked = False get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -178,25 +172,15 @@ class MqttLock(MqttEntity, LockEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - @property - def is_locked(self) -> bool: - """Return true if lock is locked.""" - return self._state - @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - @property - def supported_features(self) -> int: - """Flag supported features.""" - return LockEntityFeature.OPEN if CONF_PAYLOAD_OPEN in self._config else 0 - async def async_lock(self, **kwargs: Any) -> None: """Lock the device. @@ -211,7 +195,7 @@ class MqttLock(MqttEntity, LockEntity): ) if self._optimistic: # Optimistically assume that the lock has changed state. - self._state = True + self._attr_is_locked = True self.async_write_ha_state() async def async_unlock(self, **kwargs: Any) -> None: @@ -228,7 +212,7 @@ class MqttLock(MqttEntity, LockEntity): ) if self._optimistic: # Optimistically assume that the lock has changed state. - self._state = False + self._attr_is_locked = False self.async_write_ha_state() async def async_open(self, **kwargs: Any) -> None: @@ -245,5 +229,5 @@ class MqttLock(MqttEntity, LockEntity): ) if self._optimistic: # Optimistically assume that the lock unlocks when opened. - self._state = False + self._attr_is_locked = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index d37b15769ad..f90d02b11f4 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": ["paho-mqtt==1.6.1"], "dependencies": ["file_upload", "http"], - "codeowners": ["@emontnemery"], - "iot_class": "local_push" + "codeowners": ["@emontnemery", "@jbouwh"], + "iot_class": "local_push", + "quality_scale": "gold" } diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 6b181f2e4b5..6df545d7508 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -240,10 +240,11 @@ def warn_for_legacy_schema(domain: str) -> Callable[[ConfigType], ConfigType]: """Return a validator.""" nonlocal warned + # Logged error and repair can be removed from HA 2023.6 if domain in warned: return config - _LOGGER.warning( + _LOGGER.error( "Manually configured MQTT %s(s) found under platform key '%s', " "please move to the mqtt integration key, see " "https://www.home-assistant.io/integrations/%s.mqtt/#new_format", @@ -259,7 +260,7 @@ def warn_for_legacy_schema(domain: str) -> Callable[[ConfigType], ConfigType]: f"deprecated_yaml_{domain}", breaks_in_ha_version="2022.12.0", # Warning first added in 2022.6.0 is_fixable=False, - severity=IssueSeverity.WARNING, + severity=IssueSeverity.ERROR, translation_key="deprecated_yaml", translation_placeholders={ "more_info_url": f"https://www.home-assistant.io/integrations/{domain}.mqtt/#new_format", @@ -366,33 +367,6 @@ async def async_setup_entry_helper( await _async_setup_entities() -async def async_setup_platform_helper( - hass: HomeAssistant, - platform_domain: str, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - async_setup_entities: SetupEntity, -) -> None: - """Help to set up the platform for manual configured MQTT entities.""" - mqtt_data = get_mqtt_data(hass) - if mqtt_data.reload_entry: - _LOGGER.debug( - "MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry", - platform_domain, - ) - return - if not (entry_status := mqtt_config_entry_enabled(hass)): - _LOGGER.warning( - "MQTT integration is %s, skipping setup of manually configured MQTT %s", - "not setup" if entry_status is None else "disabled", - platform_domain, - ) - return - # Ensure we set config_entry when entries are set up to enable clean up - config_entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - await async_setup_entities(hass, async_add_entities, config, config_entry) - - def init_entity_id_from_config( hass: HomeAssistant, entity: Entity, config: ConfigType, entity_id_format: str ) -> None: @@ -646,7 +620,8 @@ async def cleanup_device_registry( def get_discovery_hash(discovery_data: DiscoveryInfoType) -> tuple[str, str]: """Get the discovery hash from the discovery data.""" - return discovery_data[ATTR_DISCOVERY_HASH] + discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] + return discovery_hash def send_discovery_done(hass: HomeAssistant, discovery_data: DiscoveryInfoType) -> None: @@ -1107,7 +1082,7 @@ class MqttEntity( payload: PublishPayloadType, qos: int = 0, retain: bool = False, - encoding: str = DEFAULT_ENCODING, + encoding: str | None = DEFAULT_ENCODING, ) -> None: """Publish message to an MQTT topic.""" log_message(self.hass, self.entity_id, topic, payload, qos, retain) @@ -1139,7 +1114,7 @@ class MqttEntity( @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._config[CONF_ENABLED_BY_DEFAULT] + return bool(self._config[CONF_ENABLED_BY_DEFAULT]) @property def entity_category(self) -> EntityCategory | None: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 363956cc732..aaef5e3e3e8 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, TypedDict, Union import attr +from homeassistant.backports.enum import StrEnum from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import template @@ -26,7 +27,13 @@ if TYPE_CHECKING: from .discovery import MQTTDiscoveryPayload from .tag import MQTTTagScanner -_SENTINEL = object() + +class PayloadSentinel(StrEnum): + """Sentinel for `async_render_with_possible_json_value`.""" + + NONE = "none" + DEFAULT = "default" + _LOGGER = logging.getLogger(__name__) @@ -143,9 +150,9 @@ class MqttCommandTemplate: if self._entity: values[ATTR_ENTITY_ID] = self._entity.entity_id values[ATTR_NAME] = self._entity.name - if not self._template_state: + if not self._template_state and self._command_template.hass is not None: self._template_state = template.TemplateStateFromEntityId( - self._command_template.hass, self._entity.entity_id + self._entity.hass, self._entity.entity_id ) values[ATTR_THIS] = self._template_state @@ -189,10 +196,12 @@ class MqttValueTemplate: def async_render_with_possible_json_value( self, payload: ReceivePayloadType, - default: ReceivePayloadType | object = _SENTINEL, + default: ReceivePayloadType | PayloadSentinel = PayloadSentinel.NONE, variables: TemplateVarsType = None, ) -> ReceivePayloadType: """Render with possible json value or pass-though a received MQTT value.""" + rendered_payload: ReceivePayloadType + if self._value_template is None: return payload @@ -213,16 +222,19 @@ class MqttValueTemplate: ) values[ATTR_THIS] = self._template_state - if default == _SENTINEL: + if default is PayloadSentinel.NONE: _LOGGER.debug( "Rendering incoming payload '%s' with variables %s and %s", payload, values, self._value_template, ) - return self._value_template.async_render_with_possible_json_value( - payload, variables=values + rendered_payload = ( + self._value_template.async_render_with_possible_json_value( + payload, variables=values + ) ) + return rendered_payload _LOGGER.debug( "Rendering incoming payload '%s' with variables %s with default value '%s' and %s", @@ -231,9 +243,10 @@ class MqttValueTemplate: default, self._value_template, ) - return self._value_template.async_render_with_possible_json_value( + rendered_payload = self._value_template.async_render_with_possible_json_value( payload, default, variables=values ) + return rendered_payload class EntityTopicState: diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 25ef7af8d6e..6d56354368e 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,6 +1,7 @@ """Configure number in a device through MQTT topic.""" from __future__ import annotations +from collections.abc import Callable import functools import logging @@ -12,7 +13,6 @@ from homeassistant.components.number import ( DEFAULT_MIN_VALUE, DEFAULT_STEP, DEVICE_CLASSES_SCHEMA, - NumberDeviceClass, NumberMode, RestoreNumber, ) @@ -45,10 +45,15 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -71,9 +76,9 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( ) -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: """Validate that the configuration is valid, throws if it isn't.""" - if config.get(CONF_MIN) >= config.get(CONF_MAX): + if config[CONF_MIN] >= config[CONF_MAX]: raise vol.Invalid(f"'{CONF_MAX}' must be > '{CONF_MIN}'") return config @@ -102,10 +107,9 @@ PLATFORM_SCHEMA_MODERN = vol.All( validate_config, ) -# Configuring MQTT Number under the number platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Number under the number platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - validate_config, warn_for_legacy_schema(number.DOMAIN), ) @@ -115,23 +119,6 @@ DISCOVERY_SCHEMA = vol.All( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT number configured under the number platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - number.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -148,8 +135,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT number.""" async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)]) @@ -161,44 +148,55 @@ class MqttNumber(MqttEntity, RestoreNumber): _entity_id_format = number.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _optimistic: bool + _command_template: Callable[[PublishPayloadType], PublishPayloadType] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT Number.""" - self._config = config - self._optimistic = False - self._sub_state = None - - self._current_number = None - RestoreNumber.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._config = config self._optimistic = config[CONF_OPTIMISTIC] - self._templates = { - CONF_COMMAND_TEMPLATE: MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), entity=self - ).async_render, - CONF_VALUE_TEMPLATE: MqttValueTemplate( - config.get(CONF_VALUE_TEMPLATE), - entity=self, - ).async_render_with_possible_json_value, - } + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), entity=self + ).async_render + self._value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_mode = config[CONF_MODE] + self._attr_native_max_value = config[CONF_MAX] + self._attr_native_min_value = config[CONF_MIN] + self._attr_native_step = config[CONF_STEP] + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) + num_value: int | float | None + payload = str(self._value_template(msg.payload)) try: if payload == self._config[CONF_PAYLOAD_RESET]: num_value = None @@ -222,7 +220,7 @@ class MqttNumber(MqttEntity, RestoreNumber): ) return - self._current_number = num_value + self._attr_native_value = num_value get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: @@ -242,44 +240,14 @@ class MqttNumber(MqttEntity, RestoreNumber): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) if self._optimistic and ( last_number_data := await self.async_get_last_number_data() ): - self._current_number = last_number_data.native_value - - @property - def native_min_value(self) -> float: - """Return the minimum value.""" - return self._config[CONF_MIN] - - @property - def native_max_value(self) -> float: - """Return the maximum value.""" - return self._config[CONF_MAX] - - @property - def native_step(self) -> float: - """Return the increment/decrement step.""" - return self._config[CONF_STEP] - - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return self._config.get(CONF_UNIT_OF_MEASUREMENT) - - @property - def native_value(self) -> float | None: - """Return the current value.""" - return self._current_number - - @property - def mode(self) -> NumberMode: - """Return the mode of the entity.""" - return self._config[CONF_MODE] + self._attr_native_value = last_number_data.native_value async def async_set_native_value(self, value: float) -> None: """Update the current value.""" @@ -287,10 +255,10 @@ class MqttNumber(MqttEntity, RestoreNumber): if value.is_integer(): current_number = int(value) - payload = self._templates[CONF_COMMAND_TEMPLATE](current_number) + payload = self._command_template(current_number) if self._optimistic: - self._current_number = current_number + self._attr_native_value = current_number self.async_write_ha_state() await self.async_publish( @@ -305,8 +273,3 @@ class MqttNumber(MqttEntity, RestoreNumber): def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - - @property - def device_class(self) -> NumberDeviceClass | None: - """Return the device class of the sensor.""" - return self._config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index e237d70e903..3454102e5e0 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -24,7 +24,6 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) from .util import valid_publish_topic @@ -46,32 +45,15 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) -# Configuring MQTT Scenes under the scene platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Scenes under the scene platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(scene.DOMAIN), ) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT scene configured under the scene platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - scene.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -88,8 +70,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT scene.""" async_add_entities([MqttScene(hass, config, config_entry, discovery_data)]) @@ -103,23 +85,29 @@ class MqttScene( _entity_id_format = scene.DOMAIN + ".{}" - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT scene.""" MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._config = config - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" async def async_activate(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 12593550e2f..d574cf081ba 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -1,6 +1,7 @@ """Configure select in a device through MQTT topic.""" from __future__ import annotations +from collections.abc import Callable import functools import logging @@ -31,10 +32,15 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -61,32 +67,14 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Select under the select platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Select under the select platform key was deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(select.DOMAIN), ) DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT select configured under the select platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - select.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -103,8 +91,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT select.""" async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)]) @@ -114,53 +102,55 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): """representation of an MQTT select.""" _entity_id_format = select.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED + _command_template: Callable[[PublishPayloadType], PublishPayloadType] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + _optimistic: bool = False - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT select.""" - self._config = config - self._optimistic = False - self._sub_state = None - - self._attr_current_option = None - SelectEntity.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_current_option = None self._optimistic = config[CONF_OPTIMISTIC] self._attr_options = config[CONF_OPTIONS] - self._templates = { - CONF_COMMAND_TEMPLATE: MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), entity=self - ).async_render, - CONF_VALUE_TEMPLATE: MqttValueTemplate( - config.get(CONF_VALUE_TEMPLATE), - entity=self, - ).async_render_with_possible_json_value, - } + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), + entity=self, + ).async_render + self._value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) - + payload = str(self._value_template(msg.payload)) if payload.lower() == "none": - payload = None + self._attr_current_option = None + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + return - if payload is not None and payload not in self.options: + if payload not in self.options: _LOGGER.error( "Invalid option for %s: '%s' (valid options: %s)", self.entity_id, @@ -168,7 +158,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): self.options, ) return - self._attr_current_option = payload get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -189,7 +178,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -198,7 +187,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): async def async_select_option(self, option: str) -> None: """Update the current value.""" - payload = self._templates[CONF_COMMAND_TEMPLATE](option) + payload = self._command_template(option) if self._optimistic: self._attr_current_option = option self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 52ba1a7e3c2..092ab88b28d 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,9 +1,11 @@ """Support for MQTT sensors.""" from __future__ import annotations +from collections.abc import Callable from datetime import datetime, timedelta import functools import logging +from typing import Any import voluptuous as vol @@ -15,6 +17,7 @@ from homeassistant.components.sensor import ( STATE_CLASSES_SCHEMA, RestoreSensor, SensorDeviceClass, + SensorExtraStoredData, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,11 +29,11 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from . import subscription @@ -42,10 +45,14 @@ from .mixins import ( MqttAvailability, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import ( + MqttValueTemplate, + PayloadSentinel, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -65,7 +72,7 @@ DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False -def validate_options(conf): +def validate_options(conf: ConfigType) -> ConfigType: """Validate options. If last reset topic is present it must be same as the state topic. @@ -107,11 +114,8 @@ PLATFORM_SCHEMA_MODERN = vol.All( validate_options, ) -# Configuring MQTT Sensors under the sensor platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Sensors under the sensor platform key was deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_LAST_RESET_TOPIC), - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - validate_options, warn_for_legacy_schema(sensor.DOMAIN), ) @@ -122,23 +126,6 @@ DISCOVERY_SCHEMA = vol.All( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT sensors configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - sensor.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -155,8 +142,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT sensor.""" async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) @@ -168,25 +155,29 @@ class MqttSensor(MqttEntity, RestoreSensor): _entity_id_format = ENTITY_ID_FORMAT _attr_last_reset = None _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED + _expire_after: int | None + _expired: bool | None + _template: Callable[[ReceivePayloadType, PayloadSentinel], ReceivePayloadType] + _last_reset_template: Callable[[ReceivePayloadType], ReceivePayloadType] - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the sensor.""" - self._state = None - self._expiration_trigger = None - - expire_after = config.get(CONF_EXPIRE_AFTER) - if expire_after is not None and expire_after > 0: - self._expired = True - else: - self._expired = None - + self._expiration_trigger: CALLBACK_TYPE | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) async def mqtt_async_added_to_hass(self) -> None: """Restore state for entities with expire_after set.""" + last_state: State | None + last_sensor_data: SensorExtraStoredData | None if ( - (expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None - and expire_after > 0 + (_expire_after := self._expire_after) is not None + and _expire_after > 0 and (last_state := await self.async_get_last_state()) is not None and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and (last_sensor_data := await self.async_get_last_sensor_data()) @@ -195,13 +186,13 @@ class MqttSensor(MqttEntity, RestoreSensor): # MqttEntity.async_added_to_hass(), then we should not restore state and not self._expiration_trigger ): - expiration_at = last_state.last_changed + timedelta(seconds=expire_after) + expiration_at = last_state.last_changed + timedelta(seconds=_expire_after) if expiration_at < (time_now := dt_util.utcnow()): # Skip reactivating the sensor _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) return self._expired = False - self._state = last_sensor_data.native_value + self._attr_native_value = last_sensor_data.native_value self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at @@ -223,13 +214,23 @@ class MqttSensor(MqttEntity, RestoreSensor): await MqttEntity.async_will_remove_from_hass(self) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_force_update = config[CONF_FORCE_UPDATE] + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_state_class = config.get(CONF_STATE_CLASS) + + self._expire_after = config.get(CONF_EXPIRE_AFTER) + if self._expire_after is not None and self._expire_after > 0: + self._expired = True + else: + self._expired = None + self._template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value @@ -237,15 +238,14 @@ class MqttSensor(MqttEntity, RestoreSensor): self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, dict[str, Any]] = {} - def _update_state(msg): + def _update_state(msg: ReceiveMessage) -> None: # auto-expire enabled? - expire_after = self._config.get(CONF_EXPIRE_AFTER) - if expire_after is not None and expire_after > 0: - # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + if self._expire_after is not None and self._expire_after > 0: + # When self._expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message self._expired = False # Reset old trigger @@ -253,35 +253,40 @@ class MqttSensor(MqttEntity, RestoreSensor): self._expiration_trigger() # Set new trigger - expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) + expiration_at = dt_util.utcnow() + timedelta(seconds=self._expire_after) self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at ) - payload = self._template(msg.payload, default=self._state) - - if payload is not None and self.device_class in ( + payload = self._template(msg.payload, PayloadSentinel.DEFAULT) + if payload is PayloadSentinel.DEFAULT: + return + if self.device_class not in { SensorDeviceClass.DATE, SensorDeviceClass.TIMESTAMP, - ): - if (payload := dt_util.parse_datetime(payload)) is None: - _LOGGER.warning( - "Invalid state message '%s' from '%s'", msg.payload, msg.topic - ) - elif self.device_class == SensorDeviceClass.DATE: - payload = payload.date() + }: + self._attr_native_value = str(payload) + return + if (payload_datetime := dt_util.parse_datetime(str(payload))) is None: + _LOGGER.warning( + "Invalid state message '%s' from '%s'", msg.payload, msg.topic + ) + self._attr_native_value = None + return + if self.device_class == SensorDeviceClass.DATE: + self._attr_native_value = payload_datetime.date() + return + self._attr_native_value = payload_datetime - self._state = payload - - def _update_last_reset(msg): + def _update_last_reset(msg: ReceiveMessage) -> None: payload = self._last_reset_template(msg.payload) if not payload: _LOGGER.debug("Ignoring empty last_reset message from '%s'", msg.topic) return try: - last_reset = dt_util.parse_datetime(payload) + last_reset = dt_util.parse_datetime(str(payload)) if last_reset is None: raise ValueError self._attr_last_reset = last_reset @@ -292,7 +297,7 @@ class MqttSensor(MqttEntity, RestoreSensor): @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" _update_state(msg) if CONF_LAST_RESET_VALUE_TEMPLATE in self._config and ( @@ -311,7 +316,7 @@ class MqttSensor(MqttEntity, RestoreSensor): @callback @log_messages(self.hass, self.entity_id) - def last_reset_message_received(msg): + def last_reset_message_received(msg: ReceiveMessage) -> None: """Handle new last_reset messages.""" _update_last_reset(msg) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -331,42 +336,21 @@ class MqttSensor(MqttEntity, RestoreSensor): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @callback - def _value_is_expired(self, *_): + def _value_is_expired(self, *_: datetime) -> None: """Triggered when value is expired.""" self._expiration_trigger = None self._expired = True self.async_write_ha_state() - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return self._config.get(CONF_UNIT_OF_MEASUREMENT) - - @property - def native_value(self) -> StateType | datetime: - """Return the state of the entity.""" - return self._state - - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._config.get(CONF_DEVICE_CLASS) - - @property - def state_class(self) -> str | None: - """Return the state class of the sensor.""" - return self._config.get(CONF_STATE_CLASS) - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" - expire_after = self._config.get(CONF_EXPIRE_AFTER) # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 return MqttAvailability.available.fget(self) and ( # type: ignore[attr-defined] - expire_after is None or not self._expired + self._expire_after is None or not self._expired ) diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 2ab226e44c0..350f02427e5 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -1,10 +1,11 @@ """Support for MQTT sirens.""" from __future__ import annotations +from collections.abc import Callable import copy import functools import logging -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -17,6 +18,7 @@ from homeassistant.components.siren import ( TURN_ON_SCHEMA, SirenEntity, SirenEntityFeature, + SirenTurnOnServiceParameters, process_turn_on_params, ) from homeassistant.config_entries import ConfigEntry @@ -30,7 +32,8 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType from . import subscription from .config import MQTT_RW_SCHEMA @@ -50,10 +53,15 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" @@ -89,9 +97,9 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Sirens under the siren platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Sirens under the siren platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(siren.DOMAIN), ) @@ -117,23 +125,6 @@ SUPPORTED_ATTRIBUTES = { _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT sirens configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - siren.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -150,8 +141,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT siren.""" async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)]) @@ -163,28 +154,30 @@ class MqttSiren(MqttEntity, SirenEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SIREN_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _command_templates: dict[ + str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType] | None + ] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + _state_on: str + _state_off: str + _optimistic: bool + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT siren.""" - self._attr_name = config[CONF_NAME] - self._attr_should_poll = False - self._supported_features = SUPPORTED_BASE - self._attr_is_on = None - self._state_on = None - self._state_off = None - self._optimistic = None - - self._attr_extra_state_attributes: dict[str, Any] = {} - - self.target = None - - super().__init__(hass, config, config_entry, discovery_data) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" state_on = config.get(CONF_STATE_ON) @@ -193,25 +186,29 @@ class MqttSiren(MqttEntity, SirenEntity): state_off = config.get(CONF_STATE_OFF) self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] + self._attr_extra_state_attributes = {} + + _supported_features = SUPPORTED_BASE if config[CONF_SUPPORT_DURATION]: - self._supported_features |= SirenEntityFeature.DURATION + _supported_features |= SirenEntityFeature.DURATION self._attr_extra_state_attributes[ATTR_DURATION] = None if config.get(CONF_AVAILABLE_TONES): - self._supported_features |= SirenEntityFeature.TONES + _supported_features |= SirenEntityFeature.TONES self._attr_available_tones = config[CONF_AVAILABLE_TONES] self._attr_extra_state_attributes[ATTR_TONE] = None if config[CONF_SUPPORT_VOLUME_SET]: - self._supported_features |= SirenEntityFeature.VOLUME_SET + _supported_features |= SirenEntityFeature.VOLUME_SET self._attr_extra_state_attributes[ATTR_VOLUME_LEVEL] = None + self._attr_supported_features = _supported_features self._optimistic = config[CONF_OPTIMISTIC] or CONF_STATE_TOPIC not in config self._attr_is_on = False if self._optimistic else None - command_template = config.get(CONF_COMMAND_TEMPLATE) - command_off_template = config.get(CONF_COMMAND_OFF_TEMPLATE) or config.get( - CONF_COMMAND_TEMPLATE + command_template: Template | None = config.get(CONF_COMMAND_TEMPLATE) + command_off_template: Template | None = ( + config.get(CONF_COMMAND_OFF_TEMPLATE) or command_template ) self._command_templates = { CONF_COMMAND_TEMPLATE: MqttCommandTemplate( @@ -230,12 +227,12 @@ class MqttSiren(MqttEntity, SirenEntity): entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT state messages.""" payload = self._value_template(msg.payload) if not payload or payload == PAYLOAD_EMPTY_JSON: @@ -245,7 +242,7 @@ class MqttSiren(MqttEntity, SirenEntity): msg.topic, ) return - json_payload = {} + json_payload: dict[str, Any] = {} if payload in [self._state_on, self._state_off, PAYLOAD_NONE]: json_payload = {STATE: payload} else: @@ -275,7 +272,8 @@ class MqttSiren(MqttEntity, SirenEntity): if json_payload: # process attributes try: - vol.All(TURN_ON_SCHEMA)(json_payload) + params: SirenTurnOnServiceParameters + params = vol.All(TURN_ON_SCHEMA)(json_payload) except vol.MultipleInvalid as invalid_siren_parameters: _LOGGER.warning( "Unable to update siren state attributes from payload '%s': %s", @@ -283,7 +281,7 @@ class MqttSiren(MqttEntity, SirenEntity): invalid_siren_parameters, ) return - self._update(process_turn_on_params(self, json_payload)) + self._update(process_turn_on_params(self, params)) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: @@ -303,7 +301,7 @@ class MqttSiren(MqttEntity, SirenEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -322,11 +320,6 @@ class MqttSiren(MqttEntity, SirenEntity): attributes.update(self._attr_extra_state_attributes) return attributes - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - async def _async_publish( self, topic: str, @@ -335,15 +328,14 @@ class MqttSiren(MqttEntity, SirenEntity): variables: dict[str, Any] | None = None, ) -> None: """Publish MQTT payload with optional command template.""" - template_variables = {STATE: value} + template_variables: dict[str, Any] = {STATE: value} if variables is not None: template_variables.update(variables) - payload = ( - self._command_templates[template](value, template_variables) - if self._command_templates[template] - else json_dumps(template_variables) - ) - if payload and payload not in PAYLOAD_NONE: + if command_template := self._command_templates[template]: + payload = command_template(value, template_variables) + else: + payload = json_dumps(template_variables) + if payload and str(payload) != PAYLOAD_NONE: await self.async_publish( self._config[topic], payload, @@ -367,7 +359,7 @@ class MqttSiren(MqttEntity, SirenEntity): # Optimistically assume that siren has changed state. _LOGGER.debug("Writing state attributes %s", kwargs) self._attr_is_on = True - self._update(kwargs) + self._update(cast(SirenTurnOnServiceParameters, kwargs)) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -386,8 +378,8 @@ class MqttSiren(MqttEntity, SirenEntity): self._attr_is_on = False self.async_write_ha_state() - def _update(self, data: dict[str, Any]) -> None: + def _update(self, data: SirenTurnOnServiceParameters) -> None: """Update the extra siren state attributes.""" for attribute, support in SUPPORTED_ATTRIBUTES.items(): - if self._supported_features & support and attribute in data: - self._attr_extra_state_attributes[attribute] = data[attribute] + if self._attr_supported_features & support and attribute in data: + self._attr_extra_state_attributes[attribute] = data[attribute] # type: ignore[literal-required] diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 4799b45e631..0ef5ea29068 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -19,15 +19,18 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "advanced_options": "Advanced options", - "certificate": "Path to custom CA certificate file", + "certificate": "Upload custom CA certificate file", "client_id": "Client ID (leave empty to randomly generated one)", - "client_cert": "Path to a client certificate file", - "client_key": "Path to a private key file", + "client_cert": "Upload client certificate file", + "client_key": "Upload private key file", "keepalive": "The time between sending keep alive messages", "tls_insecure": "Ignore broker certificate validation", "protocol": "MQTT protocol", "set_ca_cert": "Broker certificate validation", - "set_client_cert": "Use a client certificate" + "set_client_cert": "Use a client certificate", + "transport": "MQTT transport", + "ws_headers": "WebSocket headers in JSON format", + "ws_path": "WebSocket path" } }, "hassio_confirm": { @@ -47,9 +50,10 @@ "bad_will": "Invalid will topic", "bad_discovery_prefix": "Invalid discovery prefix", "bad_certificate": "The CA certificate is invalid", - "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied", + "bad_client_cert": "Invalid client certificate, ensure a PEM coded file is supplied", "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password", - "bad_client_cert_key": "Client certificate and private are no valid pair", + "bad_client_cert_key": "Client certificate and private key are not a valid pair", + "bad_ws_headers": "Supply valid HTTP headers as a JSON object", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_inclusion": "The client certificate and private key must be configurered together" } @@ -80,29 +84,32 @@ "step": { "broker": { "title": "Broker options", - "description": "Please enter the connection information of your MQTT broker.", + "description": "[%key:component::mqtt::config::step::broker::description%]", "data": { - "broker": "Broker", + "broker": "[%key:component::mqtt::config::step::broker::data::broker%]", "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "advanced_options": "Advanced options", - "certificate": "Upload custom CA certificate file", - "client_id": "Client ID (leave empty to randomly generated one)", - "client_cert": "Upload client certificate file", - "client_key": "Upload private key file", - "keepalive": "The time between sending keep alive messages", - "tls_insecure": "Ignore broker certificate validation", - "protocol": "MQTT protocol", - "set_ca_cert": "Broker certificate validation", - "set_client_cert": "Use a client certificate" + "advanced_options": "[%key:component::mqtt::config::step::broker::data::advanced_options%]", + "certificate": "[%key:component::mqtt::config::step::broker::data::certificate%]", + "client_id": "[%key:component::mqtt::config::step::broker::data::client_id%]", + "client_cert": "[%key:component::mqtt::config::step::broker::data::client_cert%]", + "client_key": "[%key:component::mqtt::config::step::broker::data::client_key%]", + "keepalive": "[%key:component::mqtt::config::step::broker::data::keepalive%]", + "tls_insecure": "[%key:component::mqtt::config::step::broker::data::tls_insecure%]", + "protocol": "[%key:component::mqtt::config::step::broker::data::protocol%]", + "set_ca_cert": "[%key:component::mqtt::config::step::broker::data::set_ca_cert%]", + "set_client_cert": "[%key:component::mqtt::config::step::broker::data::set_client_cert%]", + "transport": "[%key:component::mqtt::config::step::broker::data::transport%]", + "ws_headers": "[%key:component::mqtt::config::step::broker::data::ws_headers%]", + "ws_path": "[%key:component::mqtt::config::step::broker::data::ws_path%]" } }, "options": { "title": "MQTT options", "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nDiscovery prefix - The prefix a configuration topic for automatic discovery must start with.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", "data": { - "discovery": "Enable discovery", + "discovery": "[%key:component::mqtt::config::step::hassio_confirm::data::discovery%]", "discovery_prefix": "Discovery prefix", "birth_enable": "Enable birth message", "birth_topic": "Birth message topic", @@ -118,15 +125,16 @@ } }, "error": { - "bad_birth": "Invalid birth topic", - "bad_will": "Invalid will topic", - "bad_discovery_prefix": "Invalid discovery prefix", - "bad_certificate": "The CA certificate is invalid", - "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied", - "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password", - "bad_client_cert_key": "Client certificate and private are no valid pair", + "bad_birth": "[%key:component::mqtt::config::error::bad_birth%]", + "bad_will": "[%key:component::mqtt::config::error::bad_will%]", + "bad_discovery_prefix": "[%key:component::mqtt::config::error::bad_discovery_prefix%]", + "bad_certificate": "[%key:component::mqtt::config::error::bad_certificate%]", + "bad_client_cert": "[%key:component::mqtt::config::error::bad_client_cert%]", + "bad_client_key": "[%key:component::mqtt::config::error::bad_client_key%]", + "bad_client_cert_key": "[%key:component::mqtt::config::error::bad_client_cert_key%]", + "bad_ws_headers": "[%key:component::mqtt::config::error::bad_ws_headers%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_inclusion": "The client certificate and private key must be configured together" + "invalid_inclusion": "[%key:component::mqtt::config::error::invalid_inclusion%]" } } } diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index 87f5d3882bb..e3fd5e50093 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -19,7 +19,7 @@ class EntitySubscription: """Class to hold data about an active entity topic subscription.""" hass: HomeAssistant = attr.ib() - topic: str = attr.ib() + topic: str | None = attr.ib() message_callback: MessageCallbackType = attr.ib() subscribe_task: Coroutine[Any, Any, Callable[[], None]] | None = attr.ib() unsubscribe_callback: Callable[[], None] | None = attr.ib() @@ -39,7 +39,7 @@ class EntitySubscription: other.unsubscribe_callback() # Clear debug data if it exists debug_info.remove_subscription( - self.hass, other.message_callback, other.topic + self.hass, other.message_callback, str(other.topic) ) if self.topic is None: @@ -112,7 +112,7 @@ def async_prepare_subscribe_topics( remaining.unsubscribe_callback() # Clear debug data if it exists debug_info.remove_subscription( - hass, remaining.message_callback, remaining.topic + hass, remaining.message_callback, str(remaining.topic) ) return new_state diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f8bf2f5bc6a..09e72955e63 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,17 +1,14 @@ """Support for MQTT switches.""" from __future__ import annotations +from collections.abc import Callable import functools from typing import Any import voluptuous as vol from homeassistant.components import switch -from homeassistant.components.switch import ( - DEVICE_CLASSES_SCHEMA, - SwitchDeviceClass, - SwitchEntity, -) +from homeassistant.components.switch import DEVICE_CLASSES_SCHEMA, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -26,6 +23,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -43,10 +41,9 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, - async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" @@ -69,32 +66,15 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Switches under the switch platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Switches under the switch platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), warn_for_legacy_schema(switch.DOMAIN), ) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT switch configured under the fan platform key (deprecated).""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - switch.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -111,8 +91,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT switch.""" async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) @@ -123,27 +103,35 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): _entity_id_format = switch.ENTITY_ID_FORMAT - def __init__(self, hass, config, config_entry, discovery_data): - """Initialize the MQTT switch.""" - self._state = None + _optimistic: bool + _state_on: str + _state_off: str + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] - self._state_on = None - self._state_off = None - self._optimistic = None + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the MQTT switch.""" MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" - state_on = config.get(CONF_STATE_ON) + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + + state_on: str | None = config.get(CONF_STATE_ON) self._state_on = state_on if state_on else config[CONF_PAYLOAD_ON] - state_off = config.get(CONF_STATE_OFF) + state_off: str | None = config.get(CONF_STATE_OFF) self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] self._optimistic = ( @@ -154,20 +142,20 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT state messages.""" payload = self._value_template(msg.payload) if payload == self._state_on: - self._state = True + self._attr_is_on = True elif payload == self._state_off: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -188,28 +176,18 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) if self._optimistic and (last_state := await self.async_get_last_state()): - self._state = last_state.state == STATE_ON - - @property - def is_on(self) -> bool | None: - """Return true if device is on.""" - return self._state + self._attr_is_on = last_state.state == STATE_ON @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - @property - def device_class(self) -> SwitchDeviceClass | None: - """Return the device class of the sensor.""" - return self._config.get(CONF_DEVICE_CLASS) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on. @@ -224,7 +202,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): ) if self._optimistic: # Optimistically assume that switch has changed state. - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -241,5 +219,5 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): ) if self._optimistic: # Optimistically assume that switch has changed state. - self._state = False + self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 23afae35cc9..132ab3200a1 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -1,19 +1,22 @@ """Provides tag scanning for MQTT.""" from __future__ import annotations +from collections.abc import Callable import functools import voluptuous as vol +from homeassistant.components import tag from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import MQTT_BASE_SCHEMA from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC +from .discovery import MQTTDiscoveryPayload from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, MqttDiscoveryDeviceUpdate, @@ -21,7 +24,7 @@ from .mixins import ( send_discovery_done, update_device, ) -from .models import MqttValueTemplate, ReceiveMessage +from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType from .subscription import EntitySubscription from .util import get_mqtt_data, valid_subscribe_topic @@ -87,12 +90,14 @@ def async_has_tags(hass: HomeAssistant, device_id: str) -> bool: class MQTTTagScanner(MqttDiscoveryDeviceUpdate): """MQTT Tag scanner.""" + _value_template: Callable[[ReceivePayloadType, str], ReceivePayloadType] + def __init__( self, hass: HomeAssistant, config: ConfigType, device_id: str | None, - discovery_data: dict, + discovery_data: DiscoveryInfoType, config_entry: ConfigEntry, ) -> None: """Initialize.""" @@ -111,10 +116,10 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): self, hass, discovery_data, device_id, config_entry, LOG_NAME ) - async def async_update(self, discovery_data: dict) -> None: + async def async_update(self, discovery_data: MQTTDiscoveryPayload) -> None: """Handle MQTT tag discovery updates.""" # Update tag scanner - config = PLATFORM_SCHEMA(discovery_data) + config: DiscoveryInfoType = PLATFORM_SCHEMA(discovery_data) self._config = config self._value_template = MqttValueTemplate( config.get(CONF_VALUE_TEMPLATE), @@ -127,14 +132,11 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): """Subscribe to MQTT topics.""" async def tag_scanned(msg: ReceiveMessage) -> None: - tag_id = self._value_template(msg.payload, "").strip() + tag_id = str(self._value_template(msg.payload, "")).strip() if not tag_id: # No output from template, ignore return - # Importing tag via hass.components in case it is overridden - # in a custom_components (custom_components.tag) - tag = self.hass.components.tag - await tag.async_scan_tag(tag_id, self.device_id) + await tag.async_scan_tag(self.hass, tag_id, self.device_id) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py new file mode 100644 index 00000000000..032dba66719 --- /dev/null +++ b/homeassistant/components/mqtt/text.py @@ -0,0 +1,224 @@ +"""Support for MQTT text platform.""" +from __future__ import annotations + +from collections.abc import Callable +import functools +import logging +import re +from typing import Any + +import voluptuous as vol + +from homeassistant.components import text +from homeassistant.components.text import TextEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_MODE, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_VALUE_TEMPLATE, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import subscription +from .config import MQTT_RW_SCHEMA +from .const import ( + CONF_COMMAND_TEMPLATE, + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, +) +from .debug_info import log_messages +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) +from .util import get_mqtt_data + +_LOGGER = logging.getLogger(__name__) + +CONF_MAX = "max" +CONF_MIN = "min" +CONF_PATTERN = "pattern" + +DEFAULT_NAME = "MQTT Text" +DEFAULT_OPTIMISTIC = False +DEFAULT_PAYLOAD_RESET = "None" + +MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset( + { + text.ATTR_MAX, + text.ATTR_MIN, + text.ATTR_MODE, + text.ATTR_PATTERN, + } +) + + +def valid_text_size_configuration(config: ConfigType) -> ConfigType: + """Validate that the text length configuration is valid, throws if it isn't.""" + if config[CONF_MIN] >= config[CONF_MAX]: + raise ValueError("text length min must be >= max") + if config[CONF_MAX] > MAX_LENGTH_STATE_STATE: + raise ValueError(f"max text length must be <= {MAX_LENGTH_STATE_STATE}") + + return config + + +_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( + { + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MAX, default=MAX_LENGTH_STATE_STATE): cv.positive_int, + vol.Optional(CONF_MIN, default=0): cv.positive_int, + vol.Optional(CONF_MODE, default=text.TextMode.TEXT): vol.In( + [text.TextMode.TEXT, text.TextMode.PASSWORD] + ), + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PATTERN): cv.is_regex, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + }, +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + + +DISCOVERY_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), + valid_text_size_configuration, +) + +PLATFORM_SCHEMA_MODERN = vol.All(_PLATFORM_SCHEMA_BASE, valid_text_size_configuration) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT text through configuration.yaml and dynamically through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, text.DOMAIN, setup, DISCOVERY_SCHEMA) + + +async def _async_setup_entity( + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, +) -> None: + """Set up the MQTT text.""" + async_add_entities([MqttTextEntity(hass, config, config_entry, discovery_data)]) + + +class MqttTextEntity(MqttEntity, TextEntity): + """Representation of the MQTT text entity.""" + + _attributes_extra_blocked = MQTT_TEXT_ATTRIBUTES_BLOCKED + _entity_id_format = text.ENTITY_ID_FORMAT + + _compiled_pattern: re.Pattern[Any] | None + _optimistic: bool + _command_template: Callable[[PublishPayloadType], PublishPayloadType] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, + ) -> None: + """Initialize MQTT text entity.""" + self._attr_native_value = None + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema() -> vol.Schema: + """Return the config schema.""" + return DISCOVERY_SCHEMA + + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + self._attr_native_max = config[CONF_MAX] + self._attr_native_min = config[CONF_MIN] + self._attr_mode = config[CONF_MODE] + self._compiled_pattern = config.get(CONF_PATTERN) + self._attr_pattern = ( + self._compiled_pattern.pattern if self._compiled_pattern else None + ) + + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), + entity=self, + ).async_render + self._value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value + optimistic: bool = config[CONF_OPTIMISTIC] + self._optimistic = optimistic or config.get(CONF_STATE_TOPIC) is None + + def _prepare_subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + topics: dict[str, Any] = {} + + def add_subscription( + topics: dict[str, Any], topic: str, msg_callback: Callable + ) -> None: + if self._config.get(topic) is not None: + topics[topic] = { + "topic": self._config[topic], + "msg_callback": msg_callback, + "qos": self._config[CONF_QOS], + "encoding": self._config[CONF_ENCODING] or None, + } + + @callback + @log_messages(self.hass, self.entity_id) + def handle_state_message_received(msg: ReceiveMessage) -> None: + """Handle receiving state message via MQTT.""" + payload = str(self._value_template(msg.payload)) + self._attr_native_value = payload + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + add_subscription(topics, CONF_STATE_TOPIC, handle_state_message_received) + + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, self._sub_state, topics + ) + + async def _subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + + @property + def assumed_state(self) -> bool: + """Return true if we do optimistic updates.""" + return self._optimistic + + async def async_set_value(self, value: str) -> None: + """Change the text.""" + payload = self._command_template(value) + + await self.async_publish( + self._config[CONF_COMMAND_TOPIC], + payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + self._config[CONF_ENCODING], + ) + if self._optimistic: + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index f99f120d951..b1d5f535a67 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -6,17 +6,19 @@ }, "error": { "bad_certificate": "CA \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0431\u0440\u043e\u043a\u0435\u0440\u0430." + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0431\u0440\u043e\u043a\u0435\u0440\u0430.", + "invalid_inclusion": "\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u044f\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0438 \u0447\u0430\u0441\u0442\u043d\u0438\u044f\u0442 \u043a\u043b\u044e\u0447 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0437\u0430\u0435\u0434\u043d\u043e" }, "step": { "broker": { "data": { "advanced_options": "\u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438", "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", - "discovery": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0442\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "client_id": "ID \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d)", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "protocol": "MQTT \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "set_client_cert": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f MQTT \u0431\u0440\u043e\u043a\u0435\u0440." @@ -32,23 +34,42 @@ }, "device_automation": { "trigger_subtype": { + "button_1": "\u041f\u044a\u0440\u0432\u043e \u043a\u043e\u043f\u0447\u0435", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_5": "\u041f\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "button_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435" } }, "options": { "error": { "bad_certificate": "CA \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_inclusion": "\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u044f\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0438 \u0447\u0430\u0441\u0442\u043d\u0438\u044f\u0442 \u043a\u043b\u044e\u0447 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0437\u0430\u0435\u0434\u043d\u043e" }, "step": { "broker": { "data": { "advanced_options": "\u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438", + "certificate": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b \u0441 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043d\u0430 CA", + "client_cert": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b \u0441 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", + "client_id": "ID \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d)", + "client_key": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b \u0441 \u0447\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "protocol": "MQTT \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "set_client_cert": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 13c91abe856..0878737057b 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -12,6 +12,7 @@ "bad_client_key": "Clau privada inv\u00e0lida, assegura't que la codificaci\u00f3 del fitxer sigui PEM i sense contrasenya", "bad_discovery_prefix": "Prefix de descobriment inv\u00e0lid", "bad_will": "T\u00f2pic del missatge d'\u00faltima voluntat ('will') inv\u00e0lid", + "bad_ws_headers": "Proporciona cap\u00e7aleres HTTP v\u00e0lides en format d'objecte JSON", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_inclusion": "El certificat de client i la clau privada s'han de configurar conjuntament" }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "Opcions avan\u00e7ades", "broker": "Broker", - "certificate": "Ruta a un fitxer de certificat CA personalitzat", - "client_cert": "Ruta a un fitxer de certificat de client", + "certificate": "Puja fitxer de certificat CA personalitzat", + "client_cert": "Puja fitxer de certificat client", "client_id": "ID de client (deixa-ho buit per generar-lo aleat\u00f2riament)", - "client_key": "Ruta a un fitxer de clau privada", - "discovery": "Habilita el descobriment autom\u00e0tic", + "client_key": "Puja fitxer de clau privada", "keepalive": "Temps entre enviaments de missatges de manteniment viu ('keep alive')", "password": "Contrasenya", "port": "Port", @@ -32,13 +32,16 @@ "set_ca_cert": "Validaci\u00f3 del certificat del 'broker'", "set_client_cert": "Utilitza un certificat de client", "tls_insecure": "Ignora la validaci\u00f3 del certificat del 'broker'", - "username": "Nom d'usuari" + "transport": "Transport MQTT", + "username": "Nom d'usuari", + "ws_headers": "Cap\u00e7aleres del WebSocket en format JSON", + "ws_path": "Ruta del WebSocket" }, "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT." }, "hassio_confirm": { "data": { - "discovery": "Habilitar descobriment autom\u00e0tic" + "discovery": "Activa el descobriment" }, "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement {addon}?", "title": "Broker MQTT via complement de Home Assistant" @@ -86,6 +89,7 @@ "bad_client_key": "Clau privada inv\u00e0lida, assegura't que la codificaci\u00f3 del fitxer sigui PEM i sense contrasenya", "bad_discovery_prefix": "Prefix de descobriment inv\u00e0lid", "bad_will": "T\u00f2pic del missatge d'\u00faltima voluntat ('will') inv\u00e0lid", + "bad_ws_headers": "Proporciona cap\u00e7aleres HTTP v\u00e0lides en format d'objecte JSON", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_inclusion": "El certificat de client i la clau privada s'han de configurar conjuntament" }, @@ -94,10 +98,10 @@ "data": { "advanced_options": "Opcions avan\u00e7ades", "broker": "Broker", - "certificate": "Puja un fitxer de certificat CA personalitzat", - "client_cert": "Puja un fitxer de certificat de client", + "certificate": "Puja fitxer de certificat CA personalitzat", + "client_cert": "Puja fitxer de certificat client", "client_id": "ID de client (deixa-ho buit per generar-lo aleat\u00f2riament)", - "client_key": "Puja un fitxer de clau privada", + "client_key": "Puja fitxer de clau privada", "keepalive": "Temps entre enviaments de missatges de manteniment viu ('keep alive')", "password": "Contrasenya", "port": "Port", @@ -105,7 +109,10 @@ "set_ca_cert": "Validaci\u00f3 del certificat del 'broker'", "set_client_cert": "Utilitza un certificat de client", "tls_insecure": "Ignora la validaci\u00f3 del certificat del 'broker'", - "username": "Nom d'usuari" + "transport": "Transport MQTT", + "username": "Nom d'usuari", + "ws_headers": "Cap\u00e7aleres del WebSocket en format JSON", + "ws_path": "Ruta del WebSocket" }, "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT.", "title": "Opcions del broker" @@ -117,7 +124,7 @@ "birth_qos": "QoS del missatge de naixement", "birth_retain": "Retenci\u00f3 del missatge de naixement", "birth_topic": "Topic del missatge de naixement", - "discovery": "Activar descobriment", + "discovery": "Activa el descobriment", "discovery_prefix": "Prefix de descobriment", "will_enable": "Activa el missatge d'\u00faltima voluntat", "will_payload": "Dades (payload) del missatge d'\u00faltima voluntat", diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index f82a3f1c973..3aeae56f56e 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -5,16 +5,23 @@ "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "bad_certificate": "Certifik\u00e1t CA je neplatn\u00fd", + "bad_client_cert_key": "Klientsk\u00fd certifik\u00e1t a soukrom\u00fd nejsou platn\u00fd p\u00e1r", + "bad_client_key": "Neplatn\u00fd soukrom\u00fd kl\u00ed\u010d, zajist\u011bte, aby byl soubor s k\u00f3dem PEM dod\u00e1n bez hesla", + "bad_ws_headers": "Zadejte platn\u00e9 HTTP hlavi\u010dky jako JSON objekt", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_inclusion": "Klientsk\u00fd certifik\u00e1t a soukrom\u00fd kl\u00ed\u010d mus\u00ed b\u00fdt nakonfigurov\u00e1ny spole\u010dn\u011b" }, "step": { "broker": { "data": { + "advanced_options": "Pokro\u010dil\u00e9 mo\u017enosti", "broker": "Broker", - "discovery": "Povolit automatick\u00e9 vyhled\u00e1v\u00e1n\u00ed za\u0159\u00edzen\u00ed", "password": "Heslo", "port": "Port", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + "username": "U\u017eivatelsk\u00e9 jm\u00e9no", + "ws_headers": "Hlavi\u010dky WebSocket ve form\u00e1tu JSON", + "ws_path": "Cesta WebSocketu" }, "description": "Zadejte informace proo p\u0159ipojen\u00ed zprost\u0159edkovatele protokolu MQTT." }, @@ -50,6 +57,7 @@ }, "options": { "error": { + "bad_ws_headers": "Zadejte platn\u00e9 HTTP hlavi\u010dky jako JSON objekt", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { @@ -57,7 +65,9 @@ "data": { "password": "Heslo", "port": "Port", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + "username": "U\u017eivatelsk\u00e9 jm\u00e9no", + "ws_headers": "Hlavi\u010dky WebSocket ve form\u00e1tu JSON", + "ws_path": "Cesta WebSocketu" }, "description": "Zadejte informace proo p\u0159ipojen\u00ed zprost\u0159edkovatele protokolu MQTT." }, diff --git a/homeassistant/components/mqtt/translations/da.json b/homeassistant/components/mqtt/translations/da.json index 9b853a2dae2..03b8539abd7 100644 --- a/homeassistant/components/mqtt/translations/da.json +++ b/homeassistant/components/mqtt/translations/da.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Aktiv\u00e9r automatisk fund", "password": "Adgangskode", "port": "Port", "username": "Brugernavn" diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index b193f18b39e..677bf5f4038 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -8,10 +8,11 @@ "bad_birth": "Ung\u00fcltiges \u201eBirth\u201c-Thema", "bad_certificate": "Das CA-Zertifikat ist ung\u00fcltig", "bad_client_cert": "Ung\u00fcltiges Client-Zertifikat. Stelle sicher, dass eine PEM-codierte Datei bereitgestellt wird", - "bad_client_cert_key": "Client-Zertifikat und privates Zertifikat sind kein g\u00fcltiges Paar", + "bad_client_cert_key": "Client-Zertifikat und privater Schl\u00fcssel sind kein g\u00fcltiges Paar", "bad_client_key": "Ung\u00fcltiger privater Schl\u00fcssel. Stelle sicher, dass eine PEM-codierte Datei ohne Passwort bereitgestellt wird", "bad_discovery_prefix": "Ung\u00fcltiges Discovery-Pr\u00e4fix", "bad_will": "Ung\u00fcltiges \u201eWill\u201c-Thema", + "bad_ws_headers": "Bereitstellung g\u00fcltiger HTTP-Header als JSON-Objekt", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_inclusion": "Das Client-Zertifikat und der private Schl\u00fcssel m\u00fcssen gemeinsam konfiguriert werden" }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "Erweiterte Optionen", "broker": "Server", - "certificate": "Pfad zur benutzerdefinierten CA-Zertifikatsdatei", - "client_cert": "Pfad zur Client-Zertifikatsdatei", + "certificate": "Hochladen einer benutzerdefinierten CA-Zertifikatsdatei", + "client_cert": "Client-Zertifikatsdatei hochladen", "client_id": "Client-ID (leer lassen, um eine zuf\u00e4llig generierte zu erhalten)", - "client_key": "Pfad zur privaten Schl\u00fcsseldatei", - "discovery": "Suche aktivieren", + "client_key": "Private Schl\u00fcsseldatei hochladen", "keepalive": "Die Zeit zwischen dem Senden von Keep-Alive-Nachrichten", "password": "Passwort", "port": "Port", @@ -32,7 +32,10 @@ "set_ca_cert": "Validierung des Broker-Zertifikats", "set_client_cert": "Ein Client-Zertifikat verwenden", "tls_insecure": "Validierung des Broker-Zertifikats ignorieren", - "username": "Benutzername" + "transport": "MQTT-Transport", + "username": "Benutzername", + "ws_headers": "WebSocket Header im JSON-Format", + "ws_path": "WebSocket Pfad" }, "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein." }, @@ -41,7 +44,7 @@ "discovery": "Suche aktivieren" }, "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Supervisor Add-on {addon} bereitgestellt wird?", - "title": "MQTT Broker per Supervisor add-on" + "title": "MQTT Broker per Supervisor Add-on" } } }, @@ -73,7 +76,7 @@ "title": "Deine manuell konfigurierte(n) MQTT-{platform}(en) erfordert Aufmerksamkeit" }, "deprecated_yaml_broker_settings": { - "description": "Die folgenden Einstellungen in \u201econfiguration.yaml\u201c wurden in den MQTT-Konfigurationseintrag migriert und \u00fcberschreiben nun die Einstellungen in \u201econfiguration.yaml\u201c:\n\u201e{deprecated_settings}\u201c \n\nBitte entferne diese Einstellungen aus \u201econfiguration.yaml\u201c und starte Home Assistant neu, um dieses Problem zu beheben. Weitere Informationen findest du in der [Dokumentation]( {more_info_url} ).", + "description": "Die folgenden Einstellungen in \u201econfiguration.yaml\u201c wurden in den MQTT-Konfigurationseintrag migriert und \u00fcberschreiben nun die Einstellungen in \u201econfiguration.yaml\u201c:\n\u201e{deprecated_settings}\u201c \n\nBitte entferne diese Einstellungen aus \u201econfiguration.yaml\u201c und starte Home Assistant neu, um dieses Problem zu beheben. Weitere Informationen findest du in der [Dokumentation]({more_info_url}).", "title": "Veraltete MQTT-Einstellungen in \u201econfiguration.yaml\u201c gefunden" } }, @@ -82,10 +85,11 @@ "bad_birth": "Ung\u00fcltiges \u201eBirth\u201c-Thema", "bad_certificate": "Das CA-Zertifikat ist ung\u00fcltig", "bad_client_cert": "Ung\u00fcltiges Client-Zertifikat. Stelle sicher, dass eine PEM-codierte Datei bereitgestellt wird", - "bad_client_cert_key": "Client-Zertifikat und privates Zertifikat sind kein g\u00fcltiges Paar", + "bad_client_cert_key": "Client-Zertifikat und privater Schl\u00fcssel sind kein g\u00fcltiges Paar", "bad_client_key": "Ung\u00fcltiger privater Schl\u00fcssel. Stelle sicher, dass eine PEM-codierte Datei ohne Passwort bereitgestellt wird", "bad_discovery_prefix": "Ung\u00fcltiges Discovery-Pr\u00e4fix", "bad_will": "Ung\u00fcltiges \u201eWill\u201c-Thema", + "bad_ws_headers": "Bereitstellung g\u00fcltiger HTTP-Header als JSON-Objekt", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_inclusion": "Das Client-Zertifikat und der private Schl\u00fcssel m\u00fcssen gemeinsam konfiguriert werden" }, @@ -93,7 +97,7 @@ "broker": { "data": { "advanced_options": "Erweiterte Optionen", - "broker": "Broker", + "broker": "Server", "certificate": "Hochladen einer benutzerdefinierten CA-Zertifikatsdatei", "client_cert": "Client-Zertifikatsdatei hochladen", "client_id": "Client-ID (leer lassen, um eine zuf\u00e4llig generierte zu erhalten)", @@ -105,7 +109,10 @@ "set_ca_cert": "Validierung des Broker-Zertifikats", "set_client_cert": "Ein Client-Zertifikat verwenden", "tls_insecure": "Validierung des Broker-Zertifikats ignorieren", - "username": "Benutzername" + "transport": "MQTT-Transport", + "username": "Benutzername", + "ws_headers": "WebSocket Header im JSON-Format", + "ws_path": "WebSocket Pfad" }, "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.", "title": "Broker-Optionen" @@ -117,7 +124,7 @@ "birth_qos": "Birth Nachricht QoS", "birth_retain": "Birth Nachricht zwischenspeichern", "birth_topic": "Thema der Birth Nachricht", - "discovery": "Erkennung aktivieren", + "discovery": "Suche aktivieren", "discovery_prefix": "Discovery-Pr\u00e4fix", "will_enable": "Letzten Willen aktivieren", "will_payload": "Nutzdaten der Letzter-Wille Nachricht", diff --git a/homeassistant/components/mqtt/translations/el.json b/homeassistant/components/mqtt/translations/el.json index 129b2de45f8..d04488af9dd 100644 --- a/homeassistant/components/mqtt/translations/el.json +++ b/homeassistant/components/mqtt/translations/el.json @@ -5,16 +5,37 @@ "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "bad_birth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2", + "bad_certificate": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc CA \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", + "bad_client_cert": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM", + "bad_client_cert_key": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2", + "bad_client_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c7\u03c9\u03c1\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "bad_discovery_prefix": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2", + "bad_will": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1", + "bad_ws_headers": "\u03a0\u03b1\u03c1\u03bf\u03c7\u03ae \u03ad\u03b3\u03ba\u03c5\u03c1\u03c9\u03bd \u03b5\u03c0\u03b9\u03ba\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03c9\u03bd HTTP \u03c9\u03c2 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf JSON", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_inclusion": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03bf\u03cd\u03bd \u03bc\u03b1\u03b6\u03af" }, "step": { "broker": { "data": { + "advanced_options": "\u03a0\u03c1\u03bf\u03b7\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2", "broker": "\u039c\u03b5\u03c3\u03af\u03c4\u03b7\u03c2", - "discovery": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2", + "certificate": "\u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd CA", + "client_cert": "\u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "client_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03c5\u03c7\u03b1\u03af\u03b1)", + "client_key": "\u039c\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03bf\u03cd \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd", + "keepalive": "\u039f \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03b7\u03c2 \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03c4\u03b7\u03c1\u03b5\u03af \u03b6\u03c9\u03bd\u03c4\u03b1\u03bd\u03ac \u03c4\u03b1 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "port": "\u0398\u03cd\u03c1\u03b1", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf MQTT", + "set_ca_cert": "\u0395\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7", + "set_client_cert": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "tls_insecure": "\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7", + "transport": "\u039c\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac MQTT", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "ws_headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2 WebSocket \u03c3\u03b5 \u03bc\u03bf\u03c1\u03c6\u03ae JSON", + "ws_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae WebSocket" }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 MQTT broker." }, @@ -53,21 +74,45 @@ "deprecated_yaml": { "description": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 {\u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1}(\u03b5\u03c2) MQTT \u03ba\u03ac\u03c4\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 `{platform}`.\n\n\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03bc\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 `mqtt` \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({more_info_url}), \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "title": "\u039f\u03b9 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b1\u03c2 MQTT {platform}(s) \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03bf\u03c7\u03ae" + }, + "deprecated_yaml_broker_settings": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03bf\u03c5 \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03bf \"configuration.yaml\" \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 MQTT \u03ba\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b8\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03bf\u03c5\u03bd \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03bf \"configuration.yaml\":\n ` {deprecated_settings} ` \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf \"configuration.yaml\" \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {more_info_url} ), \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", + "title": "\u039f\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 MQTT \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03bf \"configuration.yaml\"." } }, "options": { "error": { "bad_birth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 birth.", + "bad_certificate": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc CA \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", + "bad_client_cert": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM", + "bad_client_cert_key": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2", + "bad_client_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c7\u03c9\u03c1\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "bad_discovery_prefix": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2", "bad_will": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 will.", - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "bad_ws_headers": "\u03a0\u03b1\u03c1\u03bf\u03c7\u03ae \u03ad\u03b3\u03ba\u03c5\u03c1\u03c9\u03bd \u03b5\u03c0\u03b9\u03ba\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03c9\u03bd HTTP \u03c9\u03c2 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf JSON", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_inclusion": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03bf\u03cd\u03bd \u03bc\u03b1\u03b6\u03af" }, "step": { "broker": { "data": { + "advanced_options": "\u03a0\u03c1\u03bf\u03b7\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2", "broker": "\u039c\u03b5\u03c3\u03af\u03c4\u03b7\u03c2", + "certificate": "\u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd CA", + "client_cert": "\u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "client_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03c5\u03c7\u03b1\u03af\u03b1)", + "client_key": "\u039c\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03bf\u03cd \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd", + "keepalive": "\u039f \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03b7\u03c2 \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03c4\u03b7\u03c1\u03b5\u03af \u03b6\u03c9\u03bd\u03c4\u03b1\u03bd\u03ac \u03c4\u03b1 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "port": "\u0398\u03cd\u03c1\u03b1", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf MQTT", + "set_ca_cert": "\u0395\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7", + "set_client_cert": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "tls_insecure": "\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7", + "transport": "\u039c\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac MQTT", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "ws_headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2 WebSocket \u03c3\u03b5 \u03bc\u03bf\u03c1\u03c6\u03ae JSON", + "ws_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae WebSocket" }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT.", "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 broker" @@ -80,6 +125,7 @@ "birth_retain": "\u0394\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 birth", "birth_topic": "\u0398\u03ad\u03bc\u03b1 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 birth", "discovery": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2", + "discovery_prefix": "\u03a0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2", "will_enable": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 will", "will_payload": "\u03a9\u03c6\u03ad\u03bb\u03b9\u03bc\u03bf \u03c6\u03bf\u03c1\u03c4\u03af\u03bf \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 will", "will_qos": "QoS \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 will", diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index f60f457abd3..1f092dfdc96 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -7,11 +7,12 @@ "error": { "bad_birth": "Invalid birth topic", "bad_certificate": "The CA certificate is invalid", - "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied", - "bad_client_cert_key": "Client certificate and private are no valid pair", + "bad_client_cert": "Invalid client certificate, ensure a PEM coded file is supplied", + "bad_client_cert_key": "Client certificate and private key are not a valid pair", "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password", "bad_discovery_prefix": "Invalid discovery prefix", "bad_will": "Invalid will topic", + "bad_ws_headers": "Supply valid HTTP headers as a JSON object", "cannot_connect": "Failed to connect", "invalid_inclusion": "The client certificate and private key must be configurered together" }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "Advanced options", "broker": "Broker", - "certificate": "Path to custom CA certificate file", - "client_cert": "Path to a client certificate file", + "certificate": "Upload custom CA certificate file", + "client_cert": "Upload client certificate file", "client_id": "Client ID (leave empty to randomly generated one)", - "client_key": "Path to a private key file", - "discovery": "Enable discovery", + "client_key": "Upload private key file", "keepalive": "The time between sending keep alive messages", "password": "Password", "port": "Port", @@ -32,7 +32,10 @@ "set_ca_cert": "Broker certificate validation", "set_client_cert": "Use a client certificate", "tls_insecure": "Ignore broker certificate validation", - "username": "Username" + "transport": "MQTT transport", + "username": "Username", + "ws_headers": "WebSocket headers in JSON format", + "ws_path": "WebSocket path" }, "description": "Please enter the connection information of your MQTT broker." }, @@ -81,13 +84,14 @@ "error": { "bad_birth": "Invalid birth topic", "bad_certificate": "The CA certificate is invalid", - "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied", - "bad_client_cert_key": "Client certificate and private are no valid pair", + "bad_client_cert": "Invalid client certificate, ensure a PEM coded file is supplied", + "bad_client_cert_key": "Client certificate and private key are not a valid pair", "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password", "bad_discovery_prefix": "Invalid discovery prefix", "bad_will": "Invalid will topic", + "bad_ws_headers": "Supply valid HTTP headers as a JSON object", "cannot_connect": "Failed to connect", - "invalid_inclusion": "The client certificate and private key must be configured together" + "invalid_inclusion": "The client certificate and private key must be configurered together" }, "step": { "broker": { @@ -105,7 +109,10 @@ "set_ca_cert": "Broker certificate validation", "set_client_cert": "Use a client certificate", "tls_insecure": "Ignore broker certificate validation", - "username": "Username" + "transport": "MQTT transport", + "username": "Username", + "ws_headers": "WebSocket headers in JSON format", + "ws_path": "WebSocket path" }, "description": "Please enter the connection information of your MQTT broker.", "title": "Broker options" diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json index a69be795f77..9cccbf8658b 100644 --- a/homeassistant/components/mqtt/translations/es-419.json +++ b/homeassistant/components/mqtt/translations/es-419.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Habilitar descubrimiento", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 8c05afe997a..ea3753b3bd7 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -8,10 +8,11 @@ "bad_birth": "Tema de nacimiento no v\u00e1lido", "bad_certificate": "El certificado de la CA no es v\u00e1lido", "bad_client_cert": "Certificado de cliente no v\u00e1lido, aseg\u00farate de proporcionar un archivo codificado PEM", - "bad_client_cert_key": "Certificado de cliente y la clave privada no son un par v\u00e1lido", + "bad_client_cert_key": "El certificado de cliente y la clave privada no son un par v\u00e1lido", "bad_client_key": "Clave privada no v\u00e1lida, aseg\u00farate de que se proporcione un archivo codificado PEM sin contrase\u00f1a", "bad_discovery_prefix": "Prefijo de descubrimiento no v\u00e1lido", "bad_will": "Tema de voluntad no v\u00e1lido", + "bad_ws_headers": "Proporciona cabeceras HTTP v\u00e1lidas como un objeto JSON", "cannot_connect": "No se pudo conectar", "invalid_inclusion": "El certificado del cliente y la clave privada deben configurarse juntos" }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "Opciones avanzadas", "broker": "Br\u00f3ker", - "certificate": "Ruta al archivo de certificado de la CA personalizado", - "client_cert": "Ruta a un archivo de certificado de cliente", + "certificate": "Subir archivo de certificado de la CA personalizado", + "client_cert": "Subir archivo de certificado de cliente", "client_id": "ID de cliente (dejar vac\u00edo para generar uno aleatoriamente)", - "client_key": "Ruta a un archivo de clave privada", - "discovery": "Habilitar descubrimiento", + "client_key": "Subir archivo de clave privada", "keepalive": "El tiempo entre el env\u00edo de mensajes keep alive", "password": "Contrase\u00f1a", "port": "Puerto", @@ -32,7 +32,10 @@ "set_ca_cert": "Validaci\u00f3n del certificado del br\u00f3ker", "set_client_cert": "Utilizar un certificado de cliente", "tls_insecure": "Ignorar la validaci\u00f3n del certificado del br\u00f3ker", - "username": "Nombre de usuario" + "transport": "Transporte MQTT", + "username": "Nombre de usuario", + "ws_headers": "Cabeceras WebSocket en formato JSON", + "ws_path": "Ruta del WebSocket" }, "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT." }, @@ -82,10 +85,11 @@ "bad_birth": "Tema de nacimiento no v\u00e1lido", "bad_certificate": "El certificado de la CA no es v\u00e1lido", "bad_client_cert": "Certificado de cliente no v\u00e1lido, aseg\u00farate de proporcionar un archivo codificado PEM", - "bad_client_cert_key": "Certificado de cliente y la clave privada no son un par v\u00e1lido", + "bad_client_cert_key": "El certificado de cliente y la clave privada no son un par v\u00e1lido", "bad_client_key": "Clave privada no v\u00e1lida, aseg\u00farate de que se proporcione un archivo codificado PEM sin contrase\u00f1a", "bad_discovery_prefix": "Prefijo de descubrimiento no v\u00e1lido", "bad_will": "Tema de voluntad no v\u00e1lido", + "bad_ws_headers": "Proporciona cabeceras HTTP v\u00e1lidas como un objeto JSON", "cannot_connect": "No se pudo conectar", "invalid_inclusion": "El certificado del cliente y la clave privada deben configurarse juntos" }, @@ -105,7 +109,10 @@ "set_ca_cert": "Validaci\u00f3n del certificado del br\u00f3ker", "set_client_cert": "Utilizar un certificado de cliente", "tls_insecure": "Ignorar la validaci\u00f3n del certificado del br\u00f3ker", - "username": "Nombre de usuario" + "transport": "Transporte MQTT", + "username": "Nombre de usuario", + "ws_headers": "Cabeceras WebSocket en formato JSON", + "ws_path": "Ruta del WebSocket" }, "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT.", "title": "Opciones del br\u00f3ker" diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 373a8a64a95..fad3910f91b 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -8,10 +8,11 @@ "bad_birth": "Kehtetu loomise teavitus", "bad_certificate": "CA sertifikaat on kehtetu", "bad_client_cert": "Kehtetu kliendi sertifikaat, veendu, et on esitatud PEM-kodeeritud fail", - "bad_client_cert_key": "Kliendisertifikaat ja privaatne sertifikaat ei ole kehtiv paar", + "bad_client_cert_key": "Kliendisertifikaat ja privaatne v\u00f5ti ei ole kehtiv paar", "bad_client_key": "Kehtetu privaatv\u00f5ti, veendu, et PEM-kodeeritud fail tarnitakse ilma paroolita", "bad_discovery_prefix": "Sobimatu tuvastuse eesliide", "bad_will": "Kehtetu l\u00f5petamise teavitus", + "bad_ws_headers": "Esita korrektsed HTTP p\u00e4ised JSON objektina", "cannot_connect": "Vahendajaga ei saa \u00fchendust luua.", "invalid_inclusion": "Kliendisertifikaat ja privaatne v\u00f5ti tuleb konfigureerida koos." }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "T\u00e4psemad s\u00e4tted", "broker": "Vahendaja", - "certificate": "Tee kohandatud CA-sertifikaadifaili juurde", - "client_cert": "Kliendi serdifaili tee", + "certificate": "Lae \u00fcles kohandatud CA-sertifikaadi fail", + "client_cert": "Lae \u00fcles kliendi sertifikaadifail", "client_id": "Kliendi ID (juhuslikult genereeritud ID jaoks j\u00e4ta t\u00fchjaks)", - "client_key": "Tee privaatse v\u00f5tme faili juurde", - "discovery": "Luba automaatne avastamine", + "client_key": "Lae \u00fcles privaatv\u00f5tme fail", "keepalive": "Aegumiss\u00f5numite saatmise vaheline aeg", "password": "Salas\u00f5na", "port": "Port", @@ -32,7 +32,10 @@ "set_ca_cert": "Sertifikaadi kinnitamine", "set_client_cert": "Kasuta kliendi sertifikaati", "tls_insecure": "Eira serdi valideerimist", - "username": "Kasutajanimi" + "transport": "MQTT \u00fclekanne", + "username": "Kasutajanimi", + "ws_headers": "WebSocketi p\u00e4ised JSON vormingus", + "ws_path": "WebSocketi rada" }, "description": "Sisesta oma MQTT vahendaja andmed." }, @@ -86,6 +89,7 @@ "bad_client_key": "Kehtetu privaatv\u00f5ti, veendu, et PEM-kodeeritud fail tarnitakse ilma paroolita", "bad_discovery_prefix": "Sobimatu tuvastuse eesliide", "bad_will": "Kehtetu l\u00f5petamise teavitus", + "bad_ws_headers": "Esita korrektsed HTTP p\u00e4ised JSON objektina", "cannot_connect": "\u00dchendamine nurjus", "invalid_inclusion": "Kliendisertifikaat ja privaatne v\u00f5ti tuleb konfigureerida koos." }, @@ -105,7 +109,10 @@ "set_ca_cert": "Sertifikaadi kinnitamine", "set_client_cert": "Kasuta kliendi sertifikaati", "tls_insecure": "Eira serdi valideerimist", - "username": "Kasutajanimi" + "transport": "MQTT \u00fclekanne", + "username": "Kasutajanimi", + "ws_headers": "WebSocketi p\u00e4ised JSON vormingus", + "ws_path": "WebSocketi rada" }, "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave.", "title": "MQTT maakleri valikud" diff --git a/homeassistant/components/mqtt/translations/fi.json b/homeassistant/components/mqtt/translations/fi.json index 62bb5b0f48e..41003b522b0 100644 --- a/homeassistant/components/mqtt/translations/fi.json +++ b/homeassistant/components/mqtt/translations/fi.json @@ -7,7 +7,6 @@ "broker": { "data": { "broker": "V\u00e4litt\u00e4j\u00e4", - "discovery": "Ota etsint\u00e4 k\u00e4ytt\u00f6\u00f6n", "password": "Salasana", "port": "Portti", "username": "K\u00e4ytt\u00e4j\u00e4tunnus" diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 49d041bc0f8..3515b7cbf4c 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -13,12 +13,17 @@ "data": { "advanced_options": "Options avanc\u00e9es", "broker": "Broker", - "discovery": "Activer la d\u00e9couverte", + "certificate": "T\u00e9l\u00e9verser le certificat de l\u2019autorit\u00e9 de certification personnalis\u00e9", + "client_cert": "T\u00e9l\u00e9verser le certificat client", + "client_key": "T\u00e9l\u00e9verser la cl\u00e9 priv\u00e9e", "password": "Mot de passe", "port": "Port", "protocol": "Protocole MQTT", "set_client_cert": "Utiliser un certificat client", - "username": "Nom d'utilisateur" + "transport": "Transport MQTT", + "username": "Nom d'utilisateur", + "ws_headers": "En-t\u00eates WebSocket au format JSON", + "ws_path": "Chemin WebSocket" }, "description": "Veuillez entrer les informations de connexion de votre broker MQTT." }, @@ -60,9 +65,7 @@ }, "options": { "error": { - "bad_birth": "Sujet de la naissance non valide.", "bad_discovery_prefix": "Pr\u00e9fixe de d\u00e9couverte non valide", - "bad_will": "Sujet du testament non valide.", "cannot_connect": "\u00c9chec de connexion" }, "step": { @@ -70,11 +73,17 @@ "data": { "advanced_options": "Options avanc\u00e9es", "broker": "Broker", + "certificate": "T\u00e9l\u00e9verser le certificat de l\u2019autorit\u00e9 de certification personnalis\u00e9", + "client_cert": "T\u00e9l\u00e9verser le certificat client", + "client_key": "T\u00e9l\u00e9verser la cl\u00e9 priv\u00e9e", "password": "Mot de passe", "port": "Port", "protocol": "Protocole MQTT", "set_client_cert": "Utiliser un certificat client", - "username": "Nom d'utilisateur" + "transport": "Transport MQTT", + "username": "Nom d'utilisateur", + "ws_headers": "En-t\u00eates WebSocket au format JSON", + "ws_path": "Chemin WebSocket" }, "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", "title": "Options de courtier" diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index f67b83b243e..8a5809c76c1 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -5,25 +5,42 @@ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "bad_birth": "\u05e0\u05d5\u05e9\u05d0 \u05dc\u05d9\u05d3\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "bad_certificate": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05d4-CA \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9", + "bad_client_cert": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e7\u05d5\u05d7 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d9\u05e9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05e7\u05d5\u05d1\u05e5 \u05de\u05e7\u05d5\u05d3\u05d3 PEM \u05de\u05e1\u05d5\u05e4\u05e7", + "bad_client_cert_key": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e7\u05d5\u05d7 \u05d5\u05e4\u05e8\u05d8\u05d9 \u05d0\u05d9\u05e0\u05dd \u05d6\u05d5\u05d2 \u05d7\u05d5\u05e7\u05d9", + "bad_client_key": "\u05de\u05e4\u05ea\u05d7 \u05e4\u05e8\u05d8\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d9\u05e9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05e7\u05d5\u05d1\u05e5 \u05de\u05e7\u05d5\u05d3\u05d3 PEM \u05de\u05e1\u05d5\u05e4\u05e7 \u05dc\u05dc\u05d0 \u05e1\u05d9\u05e1\u05de\u05d4", + "bad_discovery_prefix": "\u05d2\u05d9\u05dc\u05d5\u05d9 \u05dc\u05e4\u05d9 \u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9", + "bad_will": "\u05e0\u05d5\u05e9\u05d0 \u05e6\u05d5\u05d5\u05d0\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_inclusion": "\u05d9\u05e9 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 \u05d4\u05dc\u05e7\u05d5\u05d7 \u05d5\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05e4\u05e8\u05d8\u05d9 \u05d9\u05d7\u05d3" }, "step": { "broker": { "data": { - "broker": "\u05d1\u05e8\u05d5\u05e7\u05e8", - "discovery": "\u05d0\u05e4\u05e9\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9", + "advanced_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05e7\u05d3\u05de\u05d5\u05ea", + "broker": "\u05de\u05ea\u05d5\u05d5\u05da", + "certificate": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d0\u05d9\u05e9\u05d5\u05e8 CA \u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea", + "client_cert": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d0\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e7\u05d5\u05d7", + "client_id": "\u05de\u05d6\u05d4\u05d4 \u05dc\u05e7\u05d5\u05d7 (\u05dc\u05d4\u05e9\u05d0\u05d9\u05e8 \u05e8\u05d9\u05e7 \u05dc\u05de\u05d6\u05d4\u05d4 \u05e9\u05e0\u05d5\u05e6\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05e7\u05e8\u05d0\u05d9)", + "client_key": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 \u05e4\u05e8\u05d8\u05d9", + "keepalive": "\u05d4\u05d6\u05de\u05df \u05e9\u05d1\u05d9\u05df \u05e9\u05dc\u05d9\u05d7\u05ea \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea \u05dc\u05e9\u05de\u05d5\u05e8 \u05d1\u05d7\u05d9\u05d9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "port": "\u05e4\u05ea\u05d7\u05d4", + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc MQTT", + "set_ca_cert": "\u05d0\u05d9\u05de\u05d5\u05ea \u05ea\u05e2\u05d5\u05d3\u05ea \u05de\u05ea\u05d5\u05d5\u05da", + "set_client_cert": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05ea\u05e2\u05d5\u05d3\u05ea \u05dc\u05e7\u05d5\u05d7", + "tls_insecure": "\u05d4\u05ea\u05e2\u05dc\u05de\u05d5\u05ea \u05de\u05d0\u05d9\u05de\u05d5\u05ea \u05ea\u05e2\u05d5\u05d3\u05ea \u05d4\u05de\u05ea\u05d5\u05d5\u05da", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, - "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da." + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05de\u05ea\u05d5\u05d5\u05da MQTT \u05e9\u05dc\u05da." }, "hassio_confirm": { "data": { "discovery": "\u05d0\u05d9\u05e4\u05e9\u05d5\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9" }, "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4-Home Assistant \u05db\u05da \u05e9\u05ea\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT \u05d4\u05de\u05e1\u05d5\u05e4\u05e7 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05d4\u05d4\u05e8\u05d7\u05d1\u05d4 {addon}?", - "title": "MQTT \u05d1\u05e8\u05d5\u05e7\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05e8\u05d7\u05d1\u05ea Home Assistant" + "title": "\u05de\u05ea\u05d5\u05d5\u05da MQTT \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05e8\u05d7\u05d1\u05ea Home Assistant" } } }, @@ -53,23 +70,43 @@ "deprecated_yaml": { "description": "\u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 MQTT {platform} \u05e0\u05de\u05e6\u05d0 \u05ea\u05d7\u05ea \u05de\u05e4\u05ea\u05d7 \u05e4\u05dc\u05d8\u05e4\u05d5\u05e8\u05de\u05d4 `{platform}`.\n\n\u05d9\u05e9 \u05dc\u05d4\u05e2\u05d1\u05d9\u05e8 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05dc\u05de\u05e4\u05ea\u05d7 \u05d4\u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05e6\u05d9\u05d4 `mqtt`\u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05db\u05d3\u05d9 \u05dc\u05e4\u05ea\u05d5\u05e8 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5. \u05d9\u05e9 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1[\u05ea\u05d9\u05e2\u05d5\u05d3]({more_info_url}), \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "title": "MQTT {platform} \u05d4\u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 \u05d6\u05e7\u05d5\u05e7 \u05dc\u05ea\u05e9\u05d5\u05de\u05ea \u05dc\u05d1" + }, + "deprecated_yaml_broker_settings": { + "description": "\u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05e9\u05e0\u05de\u05e6\u05d0\u05d5 \u05d1-`configuration.yaml` \u05d4\u05d5\u05e2\u05d1\u05e8\u05d5 \u05dc\u05e2\u05e8\u05da MQTT config \u05d5\u05db\u05e2\u05ea \u05d9\u05e2\u05e7\u05e4\u05d5 \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d1-`configuration.yaml`:\n`{deprecated_settings}`\n\n\u05d9\u05e9 \u05dc\u05d4\u05e1\u05d9\u05e8 \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d0\u05dc\u05d5 \u05de-`configuration.yaml` \u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05db\u05d3\u05d9 \u05dc\u05e4\u05ea\u05d5\u05e8 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5. \u05e0\u05d9\u05ea\u05df \u05dc\u05e2\u05d9\u05d9\u05df \u05d1[\u05ea\u05d9\u05e2\u05d5\u05d3]({more_info_url}), \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", + "title": "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea MQTT \u05e9\u05d4\u05d5\u05e6\u05d0\u05d5 \u05de\u05e9\u05d9\u05de\u05d5\u05e9 \u05e0\u05de\u05e6\u05d0\u05d5\u05ea \u05d1'configuration.yaml'" } }, "options": { "error": { - "bad_birth": "\u05e0\u05d5\u05e9\u05d0 \u05dc\u05d9\u05d3\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9.", - "bad_will": "\u05e0\u05d5\u05e9\u05d0 \u05d7\u05d5\u05e7 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9.", - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "bad_birth": "\u05e0\u05d5\u05e9\u05d0 \u05dc\u05d9\u05d3\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "bad_certificate": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05d4-CA \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9", + "bad_client_cert": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e7\u05d5\u05d7 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d9\u05e9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05e7\u05d5\u05d1\u05e5 \u05de\u05e7\u05d5\u05d3\u05d3 PEM \u05de\u05e1\u05d5\u05e4\u05e7", + "bad_client_cert_key": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e7\u05d5\u05d7 \u05d5\u05e4\u05e8\u05d8\u05d9 \u05d0\u05d9\u05e0\u05dd \u05d6\u05d5\u05d2 \u05d7\u05d5\u05e7\u05d9", + "bad_client_key": "\u05de\u05e4\u05ea\u05d7 \u05e4\u05e8\u05d8\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d9\u05e9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05e7\u05d5\u05d1\u05e5 \u05de\u05e7\u05d5\u05d3\u05d3 PEM \u05de\u05e1\u05d5\u05e4\u05e7 \u05dc\u05dc\u05d0 \u05e1\u05d9\u05e1\u05de\u05d4", + "bad_discovery_prefix": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05d2\u05d9\u05dc\u05d5\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea", + "bad_will": "\u05e0\u05d5\u05e9\u05d0 \u05e6\u05d5\u05d5\u05d0\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_inclusion": "\u05d9\u05e9 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 \u05d4\u05dc\u05e7\u05d5\u05d7 \u05d5\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05e4\u05e8\u05d8\u05d9 \u05d9\u05d7\u05d3" }, "step": { "broker": { "data": { - "broker": "\u05d1\u05e8\u05d5\u05e7\u05e8", + "advanced_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05e7\u05d3\u05de\u05d5\u05ea", + "broker": "\u05de\u05ea\u05d5\u05d5\u05da", + "certificate": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d0\u05d9\u05e9\u05d5\u05e8 CA \u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea", + "client_cert": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d0\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e7\u05d5\u05d7", + "client_id": "\u05de\u05d6\u05d4\u05d4 \u05dc\u05e7\u05d5\u05d7 (\u05dc\u05d4\u05e9\u05d0\u05d9\u05e8 \u05e8\u05d9\u05e7 \u05dc\u05de\u05d6\u05d4\u05d4 \u05e9\u05e0\u05d5\u05e6\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05e7\u05e8\u05d0\u05d9)", + "client_key": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 \u05e4\u05e8\u05d8\u05d9", + "keepalive": "\u05d4\u05d6\u05de\u05df \u05e9\u05d1\u05d9\u05df \u05e9\u05dc\u05d9\u05d7\u05ea \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea \u05dc\u05e9\u05de\u05d5\u05e8 \u05d1\u05d7\u05d9\u05d9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "port": "\u05e4\u05ea\u05d7\u05d4", + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc MQTT", + "set_ca_cert": "\u05d0\u05d9\u05de\u05d5\u05ea \u05ea\u05e2\u05d5\u05d3\u05ea \u05de\u05ea\u05d5\u05d5\u05da", + "set_client_cert": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05ea\u05e2\u05d5\u05d3\u05ea \u05dc\u05e7\u05d5\u05d7", + "tls_insecure": "\u05d4\u05ea\u05e2\u05dc\u05de\u05d5\u05ea \u05de\u05d0\u05d9\u05de\u05d5\u05ea \u05ea\u05e2\u05d5\u05d3\u05ea \u05d4\u05de\u05ea\u05d5\u05d5\u05da", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, - "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da.", + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05de\u05ea\u05d5\u05d5\u05da MQTT \u05e9\u05dc\u05da.", "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05d5\u05d5\u05da" }, "options": { @@ -80,13 +117,14 @@ "birth_retain": "\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 \u05e0\u05e9\u05de\u05e8\u05ea", "birth_topic": "\u05e0\u05d5\u05e9\u05d0 \u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4", "discovery": "\u05d0\u05d9\u05e4\u05e9\u05d5\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9", + "discovery_prefix": "\u05d2\u05d9\u05dc\u05d5\u05d9 \u05dc\u05e4\u05d9 \u05e7\u05d9\u05d3\u05d5\u05de\u05ea", "will_enable": "\u05d4\u05e4\u05d9\u05db\u05ea \u05d4\u05d5\u05d3\u05e2\u05d4 \u05dc\u05d6\u05de\u05d9\u05e0\u05d4", "will_payload": "\u05d4\u05d0\u05dd \u05ea\u05d5\u05db\u05df \u05de\u05e0\u05d4 \u05e9\u05dc \u05d4\u05d5\u05d3\u05e2\u05d4", "will_qos": "\u05d4\u05d0\u05dd \u05d4\u05d5\u05d3\u05e2\u05ea QoS", "will_retain": "\u05d4\u05d0\u05dd \u05d4\u05d4\u05d5\u05d3\u05e2\u05d4 \u05ea\u05d9\u05e9\u05de\u05e8", "will_topic": "\u05d4\u05d0\u05dd \u05e0\u05d5\u05e9\u05d0 \u05d4\u05d5\u05d3\u05e2\u05d4" }, - "description": "\u05d2\u05d9\u05dc\u05d5\u05d9 - \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 \u05de\u05d5\u05e4\u05e2\u05dc (\u05de\u05d5\u05de\u05dc\u05e5), Home Assistant \u05d9\u05d2\u05dc\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d5\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05de\u05e4\u05e8\u05e1\u05de\u05d9\u05dd \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea\u05dd \u05d1\u05de\u05ea\u05d5\u05d5\u05da MQTT. \u05d0\u05dd \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc, \u05db\u05dc \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d7\u05d9\u05d9\u05d1\u05ea \u05dc\u05d4\u05d9\u05e2\u05e9\u05d5\u05ea \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05dc\u05d9\u05d3\u05d4 \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05de\u05ea\u05d7\u05d1\u05e8 (\u05de\u05d7\u05d3\u05e9) \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05e8\u05e6\u05d5\u05df - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05e8\u05e6\u05d5\u05df \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05d9\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d4\u05e7\u05e9\u05e8 \u05e9\u05dc\u05d5 \u05dc\u05de\u05ea\u05d5\u05d5\u05da, \u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc \u05db\u05d9\u05d1\u05d5\u05d9 \u05e9\u05dc Home Assistant) \u05d5\u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05dc\u05d0 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc Home Assistant \u05de\u05ea\u05e8\u05e1\u05e7 \u05d0\u05d5 \u05de\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8 \u05d4\u05e8\u05e9\u05ea \u05e9\u05dc\u05d5).", + "description": "\u05d2\u05d9\u05dc\u05d5\u05d9 - \u05d0\u05dd \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d6\u05de\u05d9\u05df (\u05de\u05d5\u05de\u05dc\u05e5), Home Assistant \u05d9\u05d2\u05dc\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d5\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05de\u05e4\u05e8\u05e1\u05de\u05d9\u05dd \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05d4\u05dd \u05d1\u05de\u05ea\u05d5\u05d5\u05da MQTT. \u05d0\u05dd \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05de\u05d5\u05e9\u05d1\u05ea, \u05db\u05dc \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d7\u05d9\u05d9\u05d1\u05ea \u05dc\u05d4\u05d9\u05e2\u05e9\u05d5\u05ea \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9.\n\u05d2\u05d9\u05dc\u05d5\u05d9 \u05dc\u05e4\u05d9 \u05e7\u05d9\u05d3\u05d5\u05de\u05ea - \u05d4\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e9\u05e0\u05d5\u05e9\u05d0 \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d2\u05d9\u05dc\u05d5\u05d9 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05dc\u05d9\u05d3\u05d4 \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant (\u05de\u05d7\u05d3\u05e9) \u05d9\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05e6\u05d5\u05d5\u05d0\u05d4 - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05e6\u05d5\u05d5\u05d0\u05d4 \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05de\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc\u05d5 \u05dc\u05de\u05ea\u05d5\u05d5\u05da, \u05d4\u05df \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05e7\u05d5\u05d9 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05d9\u05d1\u05d5\u05d9 Home Assistant) \u05d5\u05d4\u05df \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05dc\u05d0 \u05e0\u05e7\u05d9 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, Home Assistant \u05e7\u05d5\u05e8\u05e1 \u05d0\u05d5 \u05de\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8 \u05d4\u05e8\u05e9\u05ea \u05e9\u05dc\u05d5).", "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT" } } diff --git a/homeassistant/components/mqtt/translations/hr.json b/homeassistant/components/mqtt/translations/hr.json index 6ddc827fff9..eef7ca62225 100644 --- a/homeassistant/components/mqtt/translations/hr.json +++ b/homeassistant/components/mqtt/translations/hr.json @@ -1,11 +1,75 @@ { "config": { + "error": { + "bad_certificate": "CA certifikat nije valjan", + "bad_client_cert": "Klijentski certifikat nije valjan, osigurajte da je isporu\u010dena PEM kodirana datoteka", + "bad_client_cert_key": "Klijentski certifikat i privatni klju\u010d nisu valjani par", + "bad_client_key": "Privatni klju\u010d nije valjan, osigurajte da je PEM kodirana datoteka isporu\u010dena bez lozinke", + "bad_discovery_prefix": "Prefiks otkrivanja nije valjan", + "cannot_connect": "Povezivanje nije uspjelo", + "invalid_inclusion": "Klijentski certifikat i privatni klju\u010d moraju biti konfigurirani zajedno" + }, "step": { "broker": { "data": { + "advanced_options": "Napredne opcije", + "broker": "Broker", + "certificate": "Put do datoteke CA certifikata", + "client_cert": "Put do datoteke klijentskog certifikata", + "client_id": "ID klijenta (ostavite prazno za nasumi\u010dno generiran)", + "client_key": "Put do datoteke privatnog klju\u010da", + "keepalive": "Vrijeme izme\u0111u slanja poruka keep alive", "password": "Lozinka", "port": "Port", + "protocol": "MQTT protokol", + "set_ca_cert": "Provjera valjanosti brokerskog certifikata", + "set_client_cert": "Kori\u0161tenje klijentskog certifikata", + "tls_insecure": "Zanemari provjeru valjanosti certifikata brokera", "username": "Korisni\u010dko ime" + }, + "description": "Unesite podatke o povezivanju svog MQTT brokera." + }, + "hassio_confirm": { + "data": { + "discovery": "Omogu\u0107i otkrivanje" + }, + "description": "\u017delite li konfigurirati Home Assistant za povezivanje s MQTT brokerom koji pru\u017ea dodatak {addon}?", + "title": "MQTT Broker putem dodatka Home Assistant" + } + } + }, + "issues": { + "deprecated_yaml_broker_settings": { + "title": "Zastarjele MQTT postavke prona\u0111ene u 'configuration.yaml'" + } + }, + "options": { + "error": { + "bad_certificate": "CA certifikat nije valjan", + "bad_client_cert": "Klijentski certifikat nije valjan, osigurajte da je isporu\u010dena PEM kodirana datoteka", + "bad_client_cert_key": "Klijentski certifikat i privatni klju\u010d nisu valjani par", + "bad_client_key": "Privatni klju\u010d nije valjan, osigurajte da je PEM kodirana datoteka isporu\u010dena bez lozinke", + "bad_discovery_prefix": "Prefiks otkrivanja nije valjan", + "invalid_inclusion": "Klijentski certifikat i privatni klju\u010d moraju biti konfigurirani zajedno" + }, + "step": { + "broker": { + "data": { + "advanced_options": "Napredne opcije", + "certificate": "Prijenos datoteke CA certifikata", + "client_cert": "Prijenos datoteke klijentskog certifikata", + "client_id": "ID klijenta (ostavite prazno za nasumi\u010dno generiran)", + "client_key": "Prijenos datoteke privatnog klju\u010da", + "keepalive": "Vrijeme izme\u0111u slanja poruka keep alive", + "protocol": "MQTT protokol", + "set_ca_cert": "Provjera valjanosti certifikata brokera", + "set_client_cert": "Kori\u0161tenje klijentskog certifikata", + "tls_insecure": "Zanemari provjeru valjanosti certifikata brokera" + } + }, + "options": { + "data": { + "discovery_prefix": "Prefiks otkrivanja" } } } diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index c643f0ef404..1a6c62d7e15 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -20,11 +20,10 @@ "data": { "advanced_options": "Speci\u00e1lis be\u00e1ll\u00edt\u00e1sok", "broker": "Br\u00f3ker", - "certificate": "Az egy\u00e9ni CA-tan\u00fas\u00edtv\u00e1nyf\u00e1jl el\u00e9r\u00e9si \u00fatja", - "client_cert": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny f\u00e1jl el\u00e9r\u00e9si \u00fatja", + "certificate": "Egy\u00e9ni CA-tan\u00fas\u00edtv\u00e1nyf\u00e1jl felt\u00f6lt\u00e9se", + "client_cert": "\u00dcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny f\u00e1jl felt\u00f6lt\u00e9se", "client_id": "\u00dcgyf\u00e9l azonos\u00edt\u00f3 (hagyja \u00fcresen a v\u00e9letlenszer\u0171en gener\u00e1lt azonos\u00edt\u00f3hoz)", - "client_key": "A priv\u00e1t kulcsf\u00e1jl el\u00e9r\u00e9si \u00fatvonala", - "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se", + "client_key": "Priv\u00e1t kulcsf\u00e1jl felt\u00f6lt\u00e9se", "keepalive": "A keep alive \u00fczenetek k\u00fcld\u00e9se k\u00f6z\u00f6tti id\u0151", "password": "Jelsz\u00f3", "port": "Port", @@ -79,15 +78,15 @@ }, "options": { "error": { - "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik.", + "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik", "bad_certificate": "A CA-tan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nytelen", "bad_client_cert": "\u00c9rv\u00e9nytelen \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny. Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dolt f\u00e1jl van megadva", "bad_client_cert_key": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny \u00e9s a priv\u00e1t tan\u00fas\u00edtv\u00e1ny nem \u00e9rv\u00e9nyes p\u00e1r", "bad_client_key": "\u00c9rv\u00e9nytelen priv\u00e1t kulcs, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dol\u00e1s\u00fa f\u00e1jlt k\u00fcld\u00f6tt jelsz\u00f3 n\u00e9lk\u00fcl", "bad_discovery_prefix": "\u00c9rv\u00e9nytelen felfedez\u00e9si el\u0151tag", - "bad_will": "\u00c9rv\u00e9nytelen 'will' topik.", + "bad_will": "\u00c9rv\u00e9nytelen 'will' topik", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_inclusion": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1nyt \u00e9s a priv\u00e1t kulcsot egy\u00fctt kell konfigur\u00e1lni" + "invalid_inclusion": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1nyt \u00e9s a mag\u00e1nkulcsot egy\u00fctt kell konfigur\u00e1lni" }, "step": { "broker": { diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 4cb0fda8a91..1dbc0710453 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -5,16 +5,37 @@ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { - "cannot_connect": "Gagal terhubung" + "bad_birth": "Topik birth tidak valid", + "bad_certificate": "Sertifikat CA tidak valid", + "bad_client_cert": "Sertifikat klien tidak valid, pastikan file dengan format PEM diberikan", + "bad_client_cert_key": "Sertifikat klien dan kunci pribadi bukan pasangan yang valid", + "bad_client_key": "Kunci pribadi tidak valid, pastikan file dengan format PEM diberikan tanpa kata sandi", + "bad_discovery_prefix": "Prefiks topik penemuan tidak valid", + "bad_will": "Topik will tidak valid", + "bad_ws_headers": "Berikan header HTTP yang valid sebagai objek JSON", + "cannot_connect": "Gagal terhubung", + "invalid_inclusion": "Sertifikat klien dan kunci pribadi harus dikonfigurasi bersama" }, "step": { "broker": { "data": { + "advanced_options": "Opsi tingkat lanjut", "broker": "Broker", - "discovery": "Aktifkan penemuan", + "certificate": "Unggah file sertifikat CA khusus", + "client_cert": "Unggah file sertifikat klien", + "client_id": "ID Klien (biarkan kosong agar dihasilkan secara acak)", + "client_key": "Unggah file kunci pribadi", + "keepalive": "Waktu antara mengirim pesan tetap hidup", "password": "Kata Sandi", "port": "Port", - "username": "Nama Pengguna" + "protocol": "Protokol MQTT", + "set_ca_cert": "Validasi sertifikat broker", + "set_client_cert": "Gunakan sertifikat klien", + "tls_insecure": "Abaikan validasi sertifikat broker", + "transport": "Transportasi MQTT", + "username": "Nama Pengguna", + "ws_headers": "Header WebSocket dalam format JSON", + "ws_path": "Jalur WebSocket" }, "description": "Masukkan informasi koneksi broker MQTT Anda." }, @@ -53,21 +74,45 @@ "deprecated_yaml": { "description": "MQTT {platform} yang dikonfigurasi secara manual ditemukan di bawah kunci platform `{platform}`.\n\nPindahkan konfigurasi ke kunci integrasi `mqtt` dan mulai ulang Home Assistant untuk memperbaiki masalah ini. Lihat [dokumentasi]({more_info_url}), untuk informasi lebih lanjut.", "title": "Entitas MQTT {platform} yang dikonfigurasi secara manual membutuhkan perhatian" + }, + "deprecated_yaml_broker_settings": { + "description": "Pengaturan berikut yang ditemukan di `configuration.yaml` dimigrasikan ke entri konfigurasi MQTT dan sekarang akan menimpa pengaturan di `configuration.yaml`:\n`{deprecated_settings}`\n\nHapus pengaturan ini dari `configuration.yaml` dan mulai ulang Home Assistant untuk memperbaiki masalah ini. Lihat [dokumentasi]({more_info_url}), untuk informasi lebih lanjut.", + "title": "Pengaturan MQTT yang usang ditemukan di `configuration.yaml`" } }, "options": { "error": { "bad_birth": "Topik birth tidak valid", + "bad_certificate": "Sertifikat CA tidak valid", + "bad_client_cert": "Sertifikat klien tidak valid, pastikan file dengan format PEM diberikan", + "bad_client_cert_key": "Sertifikat klien dan kunci pribadi bukan pasangan yang valid", + "bad_client_key": "Kunci pribadi tidak valid, pastikan file dengan format PEM diberikan tanpa kata sandi", + "bad_discovery_prefix": "Prefiks topik penemuan tidak valid", "bad_will": "Topik will tidak valid", - "cannot_connect": "Gagal terhubung" + "bad_ws_headers": "Berikan header HTTP yang valid sebagai objek JSON", + "cannot_connect": "Gagal terhubung", + "invalid_inclusion": "Sertifikat klien dan kunci pribadi harus dikonfigurasi bersama" }, "step": { "broker": { "data": { + "advanced_options": "Opsi tingkat lanjut", "broker": "Broker", + "certificate": "Unggah file sertifikat CA khusus", + "client_cert": "Unggah file sertifikat klien", + "client_id": "ID Klien (biarkan kosong agar dihasilkan secara acak)", + "client_key": "Unggah file kunci pribadi", + "keepalive": "Waktu antara mengirim pesan tetap hidup", "password": "Kata Sandi", "port": "Port", - "username": "Nama Pengguna" + "protocol": "Protokol MQTT", + "set_ca_cert": "Validasi sertifikat broker", + "set_client_cert": "Gunakan sertifikat klien", + "tls_insecure": "Abaikan validasi sertifikat broker", + "transport": "Transportasi MQTT", + "username": "Nama Pengguna", + "ws_headers": "Header WebSocket dalam format JSON", + "ws_path": "Jalur WebSocket" }, "description": "Masukkan informasi koneksi broker MQTT Anda.", "title": "Opsi broker" @@ -80,6 +125,7 @@ "birth_retain": "Simpan pesan birth", "birth_topic": "Topik pesan birth", "discovery": "Aktifkan penemuan", + "discovery_prefix": "Prefiks penemuan", "will_enable": "Aktifkan pesan 'will'", "will_payload": "Payload pesan will", "will_qos": "QoS pesan will", diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 9b94a95bdc1..a90a95da470 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -8,9 +8,11 @@ "bad_birth": "Argomento di nascita non valido", "bad_certificate": "Il certificato CA non \u00e8 valido", "bad_client_cert": "Certificato client non valido, assicurarsi che venga fornito un file codificato PEM", - "bad_client_cert_key": "Il certificato del client e il privato non sono accoppiati in modo valido", + "bad_client_cert_key": "Il certificato del client e il certificato privato non sono una coppia valida", "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password", - "bad_discovery_prefix": "Prefisso di ricerca non valido", + "bad_discovery_prefix": "Prefisso di rilevamento non valido", + "bad_will": "Argomento testamento non valido", + "bad_ws_headers": "Fornisci intestazioni HTTP valide come oggetto JSON", "cannot_connect": "Impossibile connettersi", "invalid_inclusion": "Il certificato del client e la chiave privata devono essere configurati insieme" }, @@ -19,25 +21,27 @@ "data": { "advanced_options": "Opzioni avanzate", "broker": "Broker", - "certificate": "Percorso del file del certificato CA personalizzato", - "client_cert": "Percorso per un file di certificato cliente", - "client_id": "ID cliente (lasciare vuoto per generarne uno in modo casuale)", - "client_key": "Percorso per un file della chiave privata", - "discovery": "Attiva il rilevamento", - "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento attivo", + "certificate": "Carica il file del certificato CA personalizzato", + "client_cert": "Carica il file del certificato client", + "client_id": "ID client (lasciare vuoto per generarne uno casualmente)", + "client_key": "Carica il file della chiave privata", + "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento in attivit\u00e0", "password": "Password", "port": "Porta", - "protocol": "Protocollo MQTT", + "protocol": "protocollo MQTT", "set_ca_cert": "Convalida del certificato del broker", - "set_client_cert": "Utilizzare un certificato client", - "tls_insecure": "Ignorare la convalida del certificato del broker", - "username": "Nome utente" + "set_client_cert": "Usa un certificato client", + "tls_insecure": "Ignora la convalida del certificato del broker", + "transport": "Trasporto MQTT", + "username": "Nome utente", + "ws_headers": "Intestazioni WebSocket in formato JSON", + "ws_path": "Percorso WebSocket" }, "description": "Inserisci le informazioni di connessione del tuo broker MQTT." }, "hassio_confirm": { "data": { - "discovery": "Attiva il rilevamento" + "discovery": "Abilita il rilevamento" }, "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo: {addon}?", "title": "Broker MQTT tramite il componente aggiuntivo di Home Assistant" @@ -70,18 +74,24 @@ "deprecated_yaml": { "description": "{platform} MQTT configurata manualmente trovata sotto la voce della piattaforma `{platform}`. \n\nSposta la configurazione sulla voce di integrazione `mqtt` e riavvia Home Assistant per risolvere questo problema. Consulta la [documentazione]({more_info_url}), per ulteriori informazioni.", "title": "La/e tua/e {platform} MQTT configurata/e manualmente ha/hanno bisogno di attenzione" + }, + "deprecated_yaml_broker_settings": { + "description": "Le seguenti impostazioni trovate in 'configuration.yaml' sono state migrate alla voce di configurazione MQTT e ora sovrascriveranno le impostazioni in 'configuration.yaml':\n`{deprecated_settings}`\n\nRimuovi queste impostazioni da 'configuration.yaml' e riavvia Home Assistant per risolvere il problema. Per ulteriori informazioni, vedere la [documentazione]({more_info_url}).", + "title": "Impostazioni MQTT obsolete trovate in `configuration.yaml`" } }, "options": { "error": { - "bad_birth": "Argomento birth non valido.", - "bad_certificate": "Certificato CA non valido", - "bad_client_cert_key": "Il certificato del client e il certificato privato non sono accoppiati in modo valido", + "bad_birth": "Argomento di nascita non valido", + "bad_certificate": "Il certificato CA non \u00e8 valido", + "bad_client_cert": "Certificato client non valido, assicurarsi che venga fornito un file codificato PEM", + "bad_client_cert_key": "Il certificato del client e il certificato privato non sono una coppia valida", "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password", - "bad_discovery_prefix": "Prefisso di ricerca non valido", - "bad_will": "Argomento will non valido.", + "bad_discovery_prefix": "Prefisso di rilevamento non valido", + "bad_will": "Argomento testamento non valido", + "bad_ws_headers": "Fornisci intestazioni HTTP valide come oggetto JSON", "cannot_connect": "Impossibile connettersi", - "invalid_inclusion": "Il certificato e la chiave privata del client devono essere configurati insieme" + "invalid_inclusion": "Il certificato del client e la chiave privata devono essere configurati insieme" }, "step": { "broker": { @@ -89,16 +99,20 @@ "advanced_options": "Opzioni avanzate", "broker": "Broker", "certificate": "Carica il file del certificato CA personalizzato", - "client_cert": "Carica il file del certificato cliente", - "client_id": "ID cliente (lasciare vuoto per generarne uno in modo casuale)", + "client_cert": "Carica il file del certificato client", + "client_id": "ID client (lasciare vuoto per generarne uno casualmente)", "client_key": "Carica il file della chiave privata", + "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento in attivit\u00e0", "password": "Password", "port": "Porta", - "protocol": "Protocollo MQTT", + "protocol": "protocollo MQTT", "set_ca_cert": "Convalida del certificato del broker", - "set_client_cert": "Utilizza un certificato client", + "set_client_cert": "Usa un certificato client", "tls_insecure": "Ignora la convalida del certificato del broker", - "username": "Nome utente" + "transport": "Trasporto MQTT", + "username": "Nome utente", + "ws_headers": "Intestazioni WebSocket in formato JSON", + "ws_path": "Percorso WebSocket" }, "description": "Inserisci le informazioni di connessione del tuo broker MQTT.", "title": "Opzioni del broker" @@ -110,15 +124,15 @@ "birth_qos": "QoS del messaggio birth", "birth_retain": "Persistenza del messaggio birth", "birth_topic": "Argomento del messaggio birth", - "discovery": "Attiva il rilevamento", - "discovery_prefix": "Scopri il prefisso", + "discovery": "Abilita il rilevamento", + "discovery_prefix": "Rileva prefisso", "will_enable": "Abilita il messaggio testamento", "will_payload": "Payload del messaggio testamento", "will_qos": "QoS del messaggio testamento", "will_retain": "Persistenza del messaggio will", "will_topic": "Argomento del messaggio will" }, - "description": "Rilevamento: se il rilevamento \u00e8 abilitato (consigliato), Home Assistant rilever\u00e0 automaticamente i dispositivi e le entit\u00e0 che pubblicano la loro configurazione sul broker MQTT. Se il rilevamento \u00e8 disabilitato, tutta la configurazione deve essere eseguita manualmente.\nMessaggio di nascita: il messaggio di nascita verr\u00e0 inviato ogni volta che Home Assistant si (ri)collega al broker MQTT.\nMessaggio testamento: Il messaggio testamento verr\u00e0 inviato ogni volta che Home Assistant perde la connessione al broker, sia in caso di buona (es. arresto di Home Assistant) sia in caso di cattiva (es. Home Assistant in crash o perdita della connessione di rete) disconnessione.", + "description": "Rilevamento - Se il rilevamento \u00e8 abilitato (consigliato), Home Assistant rilever\u00e0 automaticamente i dispositivi e le entit\u00e0 che pubblicano la loro configurazione sul broker MQTT. Se il rilevamento \u00e8 disabilitato, tutta la configurazione deve essere fatta manualmente.\nPrefisso di rilevamento - Il prefisso con cui deve iniziare un argomento di configurazione per il rilevamento automatico.\nMessaggio di nascita - Il messaggio di nascita viene inviato ogni volta che Home Assistant si (ri)connette al broker MQTT.\nMessaggio di testamento - Il messaggio di testamento verr\u00e0 inviato ogni volta che Home Assistant perde la connessione al broker, sia in caso di disconnessione pulita (ad esempio, Home Assistant si spegne) sia in caso di disconnessione non pulita (ad esempio, Home Assistant si blocca o perde la connessione di rete).", "title": "Opzioni MQTT" } } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 274e7666a30..2d11e594cca 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -11,7 +11,6 @@ "broker": { "data": { "broker": "Broker", - "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index 454b1e0368f..25c5db0edb2 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "\ube0c\ub85c\ucee4", - "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" diff --git a/homeassistant/components/mqtt/translations/lb.json b/homeassistant/components/mqtt/translations/lb.json index fd9cd351858..885062e4777 100644 --- a/homeassistant/components/mqtt/translations/lb.json +++ b/homeassistant/components/mqtt/translations/lb.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Entdeckung aktiv\u00e9ieren", "password": "Passwuert", "port": "Port", "username": "Benotzernumm" diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 155cf0bf07a..7fe65efd7cf 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -5,16 +5,37 @@ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "bad_birth": "Ongeldig `birth` topic", + "bad_certificate": "Het CA certificaat bestand is ongeldig", + "bad_client_cert": "Ongeldig client-certificaat, zorg voor een PEM gecodeerd bestand", + "bad_client_cert_key": "Client certificaat en priv\u00e9sleutel zijn geen geldig paar", + "bad_client_key": "Ongeldige priv\u00e9sleutel (private key), zorg voor een PEM gecodeerd bestand zonder wachtwoord", + "bad_discovery_prefix": "Ongeldig discovery voorvoegsel", + "bad_will": "Ongeldig `will` topic", + "bad_ws_headers": "Geef geldige HTTP-headers op als een JSON-object", + "cannot_connect": "Kan geen verbinding maken", + "invalid_inclusion": "Het client-certificaat en priv\u00e9sleutel moeten samen worden geconfigureerd" }, "step": { "broker": { "data": { + "advanced_options": "Geavanceerde opties", "broker": "Broker", - "discovery": "Detectie inschakelen", + "certificate": "Aangepast CA certificaatbestand uploaden", + "client_cert": "Clientcertificaatbestand uploaden", + "client_id": "Client ID (leeg laten voor een willekeurig ID)", + "client_key": "Priv\u00e9sleutelbestand bestand uploaden", + "keepalive": "Tijd tussen het verzenden van keep-a-live berichten", "password": "Wachtwoord", "port": "Poort", - "username": "Gebruikersnaam" + "protocol": "MQTT protocol", + "set_ca_cert": "Broker certificaatvalidatie", + "set_client_cert": "Gebruik een client-certificaat", + "tls_insecure": "Negeer validatie van brokercertificaten", + "transport": "MQTT transport", + "username": "Gebruikersnaam", + "ws_headers": "HTTP-headers in JSON-object formaat", + "ws_path": "WebSocket pad" }, "description": "MQTT" }, @@ -49,33 +70,64 @@ "button_triple_press": "\" {subtype} \" driemaal geklikt" } }, + "issues": { + "deprecated_yaml": { + "description": "Handmatig geconfigureerd MQTT {platform}(en) gevonden onder de platform sleutel `{platform}`.\n\nVerplaats a.u.b. deze configuratieinstellingen naar de `mqtt` configuratie sleutel en herstart Home Assistant om het probleem te verhelpen. Zie de [documentatie]({more_info_url}), voor meer informatie.", + "title": "Handmatig geconfigureerd platform {platform}(s) vereist aandacht" + }, + "deprecated_yaml_broker_settings": { + "description": "De volgende instellingen gevonden in `configuration.yaml` zijn gemigreerd naar de MQTT configuratieinstellingen en overschrijven nu de instellingen in `configuration.yaml`:\n`{deprecated_settings}`\n\nVerwijder deze instellingen van `configuration.yaml` en herstart Home Assistant om dit probleem op te lossen. Zie de [documentatie]({more_info_url}), voor meer informatie.", + "title": "Verouderde MQTT instellingen gevonden in `configuration.yaml`" + } + }, "options": { "error": { "bad_birth": "Ongeldig birth topic", + "bad_certificate": "Het CA certificaat bestand is ongeldig", + "bad_client_cert": "Ongeldig client certificaat, zorg voor een PEM gecodeerd bestand", + "bad_client_cert_key": "Client-certificaat en priv\u00e9sleutel zijn geen geldig paar", + "bad_client_key": "Ongeldige priv\u00e9sleutel (private key), zorg voor een PEM gecodeerd bestand zonder wachtwoord", + "bad_discovery_prefix": "Ongeldig discovery voorvoegsel", "bad_will": "Ongeldig will topic", - "cannot_connect": "Kan geen verbinding maken" + "bad_ws_headers": "Geef geldige HTTP-headers op als een JSON-object", + "cannot_connect": "Kan geen verbinding maken", + "invalid_inclusion": "Het client-certificaat en priv\u00e9sleutel moeten samen worden geconfigureerd" }, "step": { "broker": { "data": { + "advanced_options": "Geavanceerde opties", "broker": "Broker", + "certificate": "Upload CA certificaat bestand", + "client_cert": "Upload private certificaat bestand", + "client_id": "Client ID (leeg laten voor een willekeurig ID)", + "client_key": "Upload private key bestand", + "keepalive": "Tijd tussen het verzenden van keep-a-live berichten", "password": "Wachtwoord", "port": "Poort", - "username": "Gebruikersnaam" + "protocol": "MQTT protocol", + "set_ca_cert": "Broker certificaatvalidatie", + "set_client_cert": "Gebruik een client-certificaat", + "tls_insecure": "Negeer validatie van brokercertificaten", + "transport": "MQTT transport", + "username": "Gebruikersnaam", + "ws_headers": "WebSocket headers in JSON formaat", + "ws_path": "WebSocket pad" }, "description": "Voer de verbindingsgegevens van uw MQTT-broker in.", "title": "Broker opties" }, "options": { "data": { - "birth_enable": "Geboortebericht inschakelen", - "birth_payload": "Birth message payload", - "birth_qos": "Birth message QoS", + "birth_enable": "Geboortebericht inschakelen (birth)", + "birth_payload": "Birth bericht inhoud", + "birth_qos": "Birth bericht QoS", "birth_retain": "Verbind bericht onthouden", - "birth_topic": "Birth message onderwerp", + "birth_topic": "Birth bericht topic", "discovery": "Discovery inschakelen", - "will_enable": "Offline bericht inschakelen", - "will_payload": "Offline bericht inhoud", + "discovery_prefix": "Discovery-voorvoegsel", + "will_enable": "Offline bericht inschakelen (will)", + "will_payload": "Will bericht inhoud", "will_qos": "Offline bericht QoS", "will_retain": "Offline bericht onthouden", "will_topic": "Offline bericht topic" diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 910f47ac02a..1e56625fe5c 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -8,10 +8,11 @@ "bad_birth": "Ugyldig f\u00f8dselsemne", "bad_certificate": "CA-sertifikatet er ugyldig", "bad_client_cert": "Ugyldig klientsertifikat, s\u00f8rg for at en PEM-kodet fil leveres", - "bad_client_cert_key": "Klientsertifikat og privat er ikke noe gyldig par", + "bad_client_cert_key": "Klientsertifikat og privat n\u00f8kkel er ikke et gyldig par", "bad_client_key": "Ugyldig privat n\u00f8kkel, s\u00f8rg for at en PEM-kodet fil leveres uten passord", "bad_discovery_prefix": "Ugyldig oppdagelsesprefiks", "bad_will": "Ugyldig viljeemne", + "bad_ws_headers": "Oppgi gyldige HTTP-hoder som et JSON-objekt", "cannot_connect": "Tilkobling mislyktes", "invalid_inclusion": "Klientsertifikatet og den private n\u00f8kkelen m\u00e5 konfigureres sammen" }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "Avanserte instillinger", "broker": "Megler", - "certificate": "Bane til egendefinert CA-sertifikatfil", - "client_cert": "Bane til en klientsertifikatfil", + "certificate": "Last opp egendefinert CA-sertifikatfil", + "client_cert": "Last opp klientsertifikatfil", "client_id": "Klient-ID (la st\u00e5 tomt til tilfeldig generert)", - "client_key": "Bane til en privat n\u00f8kkelfil", - "discovery": "Aktiver oppdagelse", + "client_key": "Last opp privat n\u00f8kkelfil", "keepalive": "Tiden mellom sending hold levende meldinger", "password": "Passord", "port": "Port", @@ -32,7 +32,10 @@ "set_ca_cert": "Validering av meglersertifikat", "set_client_cert": "Bruk et klientsertifikat", "tls_insecure": "Ignorer validering av meglersertifikat", - "username": "Brukernavn" + "transport": "MQTT transport", + "username": "Brukernavn", + "ws_headers": "WebSocket-hoder i JSON-format", + "ws_path": "WebSocket-bane" }, "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler." }, @@ -82,10 +85,11 @@ "bad_birth": "Ugyldig f\u00f8dselsemne", "bad_certificate": "CA-sertifikatet er ugyldig", "bad_client_cert": "Ugyldig klientsertifikat, s\u00f8rg for at en PEM-kodet fil leveres", - "bad_client_cert_key": "Klientsertifikat og privat er ikke noe gyldig par", + "bad_client_cert_key": "Klientsertifikat og privat n\u00f8kkel er ikke et gyldig par", "bad_client_key": "Ugyldig privat n\u00f8kkel, s\u00f8rg for at en PEM-kodet fil leveres uten passord", "bad_discovery_prefix": "Ugyldig oppdagelsesprefiks", "bad_will": "Ugyldig viljeemne", + "bad_ws_headers": "Oppgi gyldige HTTP-hoder som et JSON-objekt", "cannot_connect": "Tilkobling mislyktes", "invalid_inclusion": "Klientsertifikatet og den private n\u00f8kkelen m\u00e5 konfigureres sammen" }, @@ -105,9 +109,12 @@ "set_ca_cert": "Validering av meglersertifikat", "set_client_cert": "Bruk et klientsertifikat", "tls_insecure": "Ignorer validering av meglersertifikat", - "username": "Brukernavn" + "transport": "MQTT transport", + "username": "Brukernavn", + "ws_headers": "WebSocket-hoder i JSON-format", + "ws_path": "WebSocket-bane" }, - "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", + "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler.", "title": "Megleralternativer" }, "options": { diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 0084c0a1b12..b361fa11acb 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -12,6 +12,7 @@ "bad_client_key": "Nieprawid\u0142owy klucz prywatny, upewnij si\u0119, \u017ce zakodowany plik PEM jest dostarczony bez has\u0142a", "bad_discovery_prefix": "Nieprawid\u0142owy prefiks wykrywania", "bad_will": "Nieprawid\u0142owy temat \"will\"", + "bad_ws_headers": "Podaj prawid\u0142owe nag\u0142\u00f3wki HTTP jako obiekt JSON", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_inclusion": "Certyfikat klienta i klucz prywatny musz\u0105 by\u0107 skonfigurowane razem" }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "Opcje zaawansowane", "broker": "Po\u015brednik", - "certificate": "\u015acie\u017cka do pliku z niestandardowym certyfikatem CA", - "client_cert": "\u015acie\u017cka do pliku certyfikatu klienta", + "certificate": "Prze\u015blij plik z niestandardowym certyfikatem CA", + "client_cert": "Prze\u015blij plik certyfikatu klienta", "client_id": "Identyfikator klienta (pozostaw puste, aby wygenerowa\u0107 losowo)", - "client_key": "\u015acie\u017cka do pliku klucza prywatnego", - "discovery": "W\u0142\u0105cz wykrywanie", + "client_key": "Prze\u015blij plik klucza prywatnego", "keepalive": "Czas pomi\u0119dzy wys\u0142aniem wiadomo\u015bci \"keep alive\"", "password": "Has\u0142o", "port": "Port", @@ -32,7 +32,10 @@ "set_ca_cert": "Sprawdzanie certyfikatu brokera", "set_client_cert": "U\u017cyj certyfikatu klienta", "tls_insecure": "Ignoruj sprawdzanie certyfikatu brokera", - "username": "Nazwa u\u017cytkownika" + "transport": "MQTT transport", + "username": "Nazwa u\u017cytkownika", + "ws_headers": "Nag\u0142\u00f3wki WebSocket w formacie JSON", + "ws_path": "\u015acie\u017cka WebSocket" }, "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT." }, @@ -86,6 +89,7 @@ "bad_client_key": "Nieprawid\u0142owy klucz prywatny, upewnij si\u0119, \u017ce zakodowany plik PEM jest dostarczony bez has\u0142a", "bad_discovery_prefix": "Nieprawid\u0142owy prefiks wykrywania", "bad_will": "Nieprawid\u0142owy temat \"will\"", + "bad_ws_headers": "Podaj prawid\u0142owe nag\u0142\u00f3wki HTTP jako obiekt JSON", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_inclusion": "Certyfikat klienta i klucz prywatny musz\u0105 by\u0107 skonfigurowane razem" }, @@ -105,9 +109,12 @@ "set_ca_cert": "Sprawdzanie certyfikatu brokera", "set_client_cert": "U\u017cyj certyfikatu klienta", "tls_insecure": "Ignoruj sprawdzanie certyfikatu brokera", - "username": "Nazwa u\u017cytkownika" + "transport": "MQTT transport", + "username": "Nazwa u\u017cytkownika", + "ws_headers": "Nag\u0142\u00f3wki WebSocket w formacie JSON", + "ws_path": "\u015acie\u017cka WebSocket" }, - "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT", + "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.", "title": "Opcje brokera" }, "options": { diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index d9e8ac43192..73753557d67 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -8,10 +8,11 @@ "bad_birth": "T\u00f3pico de nascimento inv\u00e1lido", "bad_certificate": "O certificado CA \u00e9 inv\u00e1lido", "bad_client_cert": "Certificado de cliente inv\u00e1lido, certifique-se de que um arquivo codificado PEM seja fornecido", - "bad_client_cert_key": "Certificado de cliente e privado n\u00e3o s\u00e3o pares v\u00e1lidos", + "bad_client_cert_key": "O certificado do cliente e a chave privada n\u00e3o s\u00e3o um par v\u00e1lido", "bad_client_key": "Chave privada inv\u00e1lida, certifique-se de que um arquivo codificado PEM seja fornecido sem senha", "bad_discovery_prefix": "Prefixo de descoberta inv\u00e1lido", "bad_will": "T\u00f3pico de vontade inv\u00e1lido", + "bad_ws_headers": "Forne\u00e7a cabe\u00e7alhos HTTP v\u00e1lidos como um objeto JSON", "cannot_connect": "Falha ao conectar", "invalid_inclusion": "O certificado do cliente e a chave privada devem ser configurados juntos" }, @@ -20,11 +21,10 @@ "data": { "advanced_options": "Op\u00e7\u00f5es avan\u00e7adas", "broker": "Endere\u00e7o do Broker", - "certificate": "Caminho para o arquivo de certificado de CA personalizado", - "client_cert": "Caminho para um arquivo de certificado de cliente", + "certificate": "Carregar arquivo de certificado de CA personalizado", + "client_cert": "Carregar arquivo de certificado do cliente", "client_id": "ID do cliente (deixe em branco para um gerado aleatoriamente)", - "client_key": "Caminho para um arquivo de chave privada", - "discovery": "Ativar descoberta", + "client_key": "Carregar arquivo de chave privada", "keepalive": "O tempo entre o envio de mensagens de manuten\u00e7\u00e3o viva", "password": "Senha", "port": "Porta", @@ -32,7 +32,10 @@ "set_ca_cert": "Valida\u00e7\u00e3o do certificado do corretor", "set_client_cert": "Usar um certificado de cliente", "tls_insecure": "Ignorar a valida\u00e7\u00e3o do certificado do corretor", - "username": "Usu\u00e1rio" + "transport": "Transporte MQTT", + "username": "Usu\u00e1rio", + "ws_headers": "Cabe\u00e7alhos WebSocket no formato JSON", + "ws_path": "Caminho do WebSocket" }, "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT." }, @@ -82,10 +85,11 @@ "bad_birth": "T\u00f3pico de nascimento inv\u00e1lido", "bad_certificate": "O certificado CA \u00e9 inv\u00e1lido", "bad_client_cert": "Certificado de cliente inv\u00e1lido, certifique-se de que um arquivo codificado PEM seja fornecido", - "bad_client_cert_key": "Certificado de cliente e privado n\u00e3o s\u00e3o pares v\u00e1lidos", + "bad_client_cert_key": "O certificado do cliente e a chave privada n\u00e3o s\u00e3o um par v\u00e1lido", "bad_client_key": "Chave privada inv\u00e1lida, certifique-se de que um arquivo codificado PEM seja fornecido sem senha", "bad_discovery_prefix": "Prefixo de descoberta inv\u00e1lido", "bad_will": "T\u00f3pico de vontade inv\u00e1lido", + "bad_ws_headers": "Forne\u00e7a cabe\u00e7alhos HTTP v\u00e1lidos como um objeto JSON", "cannot_connect": "Falha ao conectar", "invalid_inclusion": "O certificado do cliente e a chave privada devem ser configurados juntos" }, @@ -93,7 +97,7 @@ "broker": { "data": { "advanced_options": "Op\u00e7\u00f5es avan\u00e7adas", - "broker": "", + "broker": "Endere\u00e7o do Broker", "certificate": "Carregar arquivo de certificado de CA personalizado", "client_cert": "Carregar arquivo de certificado do cliente", "client_id": "ID do cliente (deixe em branco para um gerado aleatoriamente)", @@ -105,9 +109,12 @@ "set_ca_cert": "Valida\u00e7\u00e3o do certificado do corretor", "set_client_cert": "Usar um certificado de cliente", "tls_insecure": "Ignorar a valida\u00e7\u00e3o do certificado do corretor", - "username": "Usu\u00e1rio" + "transport": "Transporte MQTT", + "username": "Usu\u00e1rio", + "ws_headers": "Cabe\u00e7alhos WebSocket no formato JSON", + "ws_path": "Caminho do WebSocket" }, - "description": "Insira as informa\u00e7\u00f5es de conex\u00e3o do seu broker MQTT.", + "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT.", "title": "Op\u00e7\u00f5es do broker" }, "options": { diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json index 6ff10cf515c..47354149caf 100644 --- a/homeassistant/components/mqtt/translations/pt.json +++ b/homeassistant/components/mqtt/translations/pt.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Ativar descoberta", "password": "Palavra-passe", "port": "Porto", "username": "Nome de Utilizador" diff --git a/homeassistant/components/mqtt/translations/ro.json b/homeassistant/components/mqtt/translations/ro.json index a98818be937..e65a30ed761 100644 --- a/homeassistant/components/mqtt/translations/ro.json +++ b/homeassistant/components/mqtt/translations/ro.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Activa\u021bi descoperirea", "password": "Parol\u0103", "port": "Port", "username": "Nume de utilizator" diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index f1f241e4c78..acbf55512a2 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -12,6 +12,7 @@ "bad_client_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 PEM \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f.", "bad_discovery_prefix": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0442\u043e\u043f\u0438\u043a\u0430 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f.", "bad_will": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", + "bad_ws_headers": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 HTTP \u0432 \u0432\u0438\u0434\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 JSON.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_inclusion": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u0432\u043c\u0435\u0441\u0442\u0435." }, @@ -20,16 +21,21 @@ "data": { "advanced_options": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", - "certificate": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0426\u0421", - "client_cert": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", + "certificate": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0426\u0421", + "client_cert": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", "client_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c)", - "client_key": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430", - "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", + "client_key": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430", "keepalive": "\u0412\u0440\u0435\u043c\u044f \u043c\u0435\u0436\u0434\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Keep Alive", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b MQTT", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + "set_ca_cert": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0430", + "set_client_cert": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", + "tls_insecure": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0430", + "transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442 MQTT", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "ws_headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 WebSocket \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 JSON", + "ws_path": "\u041f\u0443\u0442\u044c \u043a WebSocket" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT." }, @@ -64,19 +70,45 @@ "button_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } }, + "issues": { + "deprecated_yaml_broker_settings": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0432 `configuration.yaml`, \u0431\u044b\u043b\u0438 \u043f\u0435\u0440\u0435\u043d\u0435\u0441\u0435\u043d\u044b \u0432 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 MQTT \u0438 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u044e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432 `configuration.yaml`:\n`{deprecated_settings}` \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u044d\u0442\u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0438\u0437 `configuration.yaml` \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\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]({more_info_url}).", + "title": "\u0423\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b MQTT \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 `configuration.yaml`" + } + }, "options": { "error": { "bad_birth": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", + "bad_certificate": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0426\u0421 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "bad_client_cert": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b, \u0437\u0430\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 PEM", + "bad_client_cert_key": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043d\u0435 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043f\u0430\u0440\u043e\u0439.", + "bad_client_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 PEM \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f.", + "bad_discovery_prefix": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0442\u043e\u043f\u0438\u043a\u0430 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f.", "bad_will": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "bad_ws_headers": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 HTTP \u0432 \u0432\u0438\u0434\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 JSON.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_inclusion": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u0432\u043c\u0435\u0441\u0442\u0435." }, "step": { "broker": { "data": { + "advanced_options": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", + "certificate": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0426\u0421", + "client_cert": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", + "client_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c)", + "client_key": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430", + "keepalive": "\u0412\u0440\u0435\u043c\u044f \u043c\u0435\u0436\u0434\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Keep Alive", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b MQTT", + "set_ca_cert": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0430", + "set_client_cert": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", + "tls_insecure": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0430", + "transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442 MQTT", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "ws_headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 WebSocket \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 JSON", + "ws_path": "\u041f\u0443\u0442\u044c \u043a WebSocket" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0411\u0440\u043e\u043a\u0435\u0440\u0430" @@ -88,14 +120,15 @@ "birth_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)", - "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", + "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", + "discovery_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_topic": "\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)" }, - "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u2014 \u0435\u0441\u043b\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f), Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u044e\u0442 \u0441\u0432\u043e\u044e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043d\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0435 MQTT. \u0415\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, \u0432\u0441\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437, \u043a\u043e\u0433\u0434\u0430 Home Assistant \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.\n\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u043a \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0442 \u0431\u0440\u043e\u043a\u0435\u0440\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Home Assistant), \u0442\u0430\u043a \u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0441\u0431\u043e\u0435 Home Assistant \u0438\u043b\u0438 \u043f\u043e\u0442\u0435\u0440\u0435 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f).", + "description": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u2014 \u0435\u0441\u043b\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f), Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u044e\u0442 \u0441\u0432\u043e\u044e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043d\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0435 MQTT. \u0415\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, \u0432\u0441\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u2014 \u043f\u0440\u0435\u0444\u0438\u043a\u0441, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043f\u0438\u043a \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\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.\n\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437, \u043a\u043e\u0433\u0434\u0430 Home Assistant \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.\n\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u043a \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0442 \u0431\u0440\u043e\u043a\u0435\u0440\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Home Assistant), \u0442\u0430\u043a \u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0441\u0431\u043e\u0435 Home Assistant \u0438\u043b\u0438 \u043f\u043e\u0442\u0435\u0440\u0435 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f).", "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b MQTT" } } diff --git a/homeassistant/components/mqtt/translations/sk.json b/homeassistant/components/mqtt/translations/sk.json index e01295844ec..25f39bf8191 100644 --- a/homeassistant/components/mqtt/translations/sk.json +++ b/homeassistant/components/mqtt/translations/sk.json @@ -1,25 +1,77 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "bad_certificate": "Certifik\u00e1t CA je neplatn\u00fd", + "bad_client_cert_key": "Certifik\u00e1t klienta a s\u00fakromn\u00fd k\u013e\u00fa\u010d nie s\u00fa platn\u00fdm p\u00e1rom", + "bad_ws_headers": "Zadajte platn\u00e9 hlavi\u010dky HTTP ako objekt JSON", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_inclusion": "Certifik\u00e1t klienta a s\u00fakromn\u00fd k\u013e\u00fa\u010d musia by\u0165 nakonfigurovan\u00e9 spolo\u010dne" }, "step": { "broker": { "data": { + "advanced_options": "Pokro\u010dil\u00e9 nastavenia", + "client_key": "Nahrajte s\u00fabor so s\u00fakromn\u00fdm k\u013e\u00fa\u010dom", "password": "Heslo", "port": "Port", + "protocol": "MQTT protokol", + "set_client_cert": "Pou\u017eite klientsky certifik\u00e1t", + "transport": "MQTT transport", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } + }, + "hassio_confirm": { + "description": "Chcete nakonfigurova\u0165 dom\u00e1ceho asistenta na pripojenie k brokerovi MQTT poskytovan\u00e9mu doplnkom {addon}?", + "title": "MQTT Broker cez doplnok Home Assistant" } } }, + "device_automation": { + "trigger_subtype": { + "button_1": "Prv\u00e9 tla\u010didlo", + "button_2": "Druh\u00e9 tla\u010didlo", + "button_3": "Tretie tla\u010didlo", + "button_4": "\u0160tvrt\u00e9 tla\u010didlo", + "button_5": "Piate tla\u010didlo", + "button_6": "\u0160ieste tla\u010didlo", + "turn_off": "Vypn\u00fa\u0165", + "turn_on": "Zapn\u00fa\u0165" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" kliknut\u00e9 dvakr\u00e1t" + } + }, "options": { + "error": { + "bad_certificate": "Certifik\u00e1t CA je neplatn\u00fd", + "bad_client_cert_key": "Certifik\u00e1t klienta a s\u00fakromn\u00fd k\u013e\u00fa\u010d nie s\u00fa platn\u00fdm p\u00e1rom", + "bad_ws_headers": "Zadajte platn\u00e9 hlavi\u010dky HTTP ako objekt JSON", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_inclusion": "Certifik\u00e1t klienta a s\u00fakromn\u00fd k\u013e\u00fa\u010d musia by\u0165 nakonfigurovan\u00e9 spolo\u010dne" + }, "step": { "broker": { "data": { + "advanced_options": "Pokro\u010dil\u00e9 nastavenia", + "client_key": "Nahrajte s\u00fabor so s\u00fakromn\u00fdm k\u013e\u00fa\u010dom", + "password": "Heslo", "port": "Port", + "protocol": "MQTT protokol", + "set_client_cert": "Pou\u017eite klientsky certifik\u00e1t", + "transport": "MQTT transport", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } + }, + "options": { + "data": { + "will_payload": "Odo\u0161le z\u00e1\u0165a\u017e", + "will_qos": "Odo\u0161le QoS" + }, + "title": "MQTT mo\u017enosti" } } } diff --git a/homeassistant/components/mqtt/translations/sl.json b/homeassistant/components/mqtt/translations/sl.json index 9f16209d524..327b33af684 100644 --- a/homeassistant/components/mqtt/translations/sl.json +++ b/homeassistant/components/mqtt/translations/sl.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "Posrednik", - "discovery": "Omogo\u010di odkrivanje", "password": "Geslo", "port": "port", "username": "Uporabni\u0161ko ime" diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 811da569992..249c8153a58 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -11,7 +11,6 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Aktivera uppt\u00e4ckt", "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/mqtt/translations/th.json b/homeassistant/components/mqtt/translations/th.json index 624df71b786..3be3637e94b 100644 --- a/homeassistant/components/mqtt/translations/th.json +++ b/homeassistant/components/mqtt/translations/th.json @@ -6,7 +6,6 @@ "step": { "broker": { "data": { - "discovery": "\u0e40\u0e1b\u0e34\u0e14\u0e43\u0e0a\u0e49\u0e01\u0e32\u0e23\u0e04\u0e49\u0e19\u0e2b\u0e32\u0e2d\u0e38\u0e1b\u0e01\u0e23\u0e13\u0e4c", "password": "\u0e23\u0e2b\u0e31\u0e2a\u0e1c\u0e48\u0e32\u0e19", "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49" } diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index eda134d8220..0cf9e430f88 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -5,15 +5,32 @@ "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" + "bad_birth": "Ge\u00e7ersiz do\u011fum konusu", + "bad_certificate": "CA sertifikas\u0131 ge\u00e7ersiz", + "bad_client_cert": "Ge\u00e7ersiz m\u00fc\u015fteri sertifikas\u0131, PEM kodlu bir dosyan\u0131n sa\u011fland\u0131\u011f\u0131ndan emin olun", + "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel ge\u00e7erli bir \u00e7ift de\u011fil", + "bad_client_key": "Ge\u00e7ersiz \u00f6zel anahtar, PEM kodlu bir dosyan\u0131n parola olmadan sa\u011fland\u0131\u011f\u0131ndan emin olun", + "bad_discovery_prefix": "Ge\u00e7ersiz ke\u015fif \u00f6neki", + "bad_will": "Ge\u00e7ersiz irade konusu", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_inclusion": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar birlikte yap\u0131land\u0131r\u0131lmal\u0131d\u0131r" }, "step": { "broker": { "data": { + "advanced_options": "Geli\u015fmi\u015f se\u00e7enekler", "broker": "Broker", - "discovery": "Ke\u015ffetmeyi etkinle\u015ftir", + "certificate": "\u00d6zel CA sertifika dosyas\u0131n\u0131n yolu", + "client_cert": "\u0130stemci sertifika dosyas\u0131n\u0131n yolu", + "client_id": "M\u00fc\u015fteri Kimli\u011fi (rastgele olu\u015fturulmu\u015f olana kadar bo\u015f b\u0131rak\u0131n)", + "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131n yolu", + "keepalive": "Canl\u0131 tutma mesajlar\u0131 g\u00f6nderme aras\u0131ndaki s\u00fcre", "password": "Parola", "port": "Port", + "protocol": "MQTT protokol\u00fc", + "set_ca_cert": "Arac\u0131 sertifikas\u0131 do\u011frulama", + "set_client_cert": "\u0130stemci sertifikas\u0131 kullan", + "tls_insecure": "Arac\u0131 sertifika do\u011frulamas\u0131n\u0131 yoksay", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin." @@ -53,20 +70,40 @@ "deprecated_yaml": { "description": "El ile yap\u0131land\u0131r\u0131lm\u0131\u015f MQTT {platform} (lar) ` {platform} ` platform anahtar\u0131 alt\u0131nda bulundu. \n\n Bu sorunu gidermek i\u00e7in l\u00fctfen yap\u0131land\u0131rmay\u0131 `mqtt` entegrasyon anahtar\u0131na ta\u015f\u0131y\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n. Daha fazla bilgi i\u00e7in [belgelere]( {more_info_url} ) bak\u0131n.", "title": "Manuel olarak yap\u0131land\u0131r\u0131lan MQTT {platform} (lar)\u0131n\u0131zla ilgilenilmesi gerekiyor" + }, + "deprecated_yaml_broker_settings": { + "description": "\"configuration.yaml\" dosyas\u0131nda bulunan a\u015fa\u011f\u0131daki ayarlar MQTT yap\u0131land\u0131rma giri\u015fine ta\u015f\u0131nd\u0131 ve \u015fimdi \"configuration.yaml\" i\u00e7indeki ayarlar\u0131 ge\u00e7ersiz k\u0131lacakt\u0131r:\n ` {deprecated_settings} ` \n\n L\u00fctfen bu ayarlar\u0131 `configuration.yaml` dosyas\u0131ndan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n. Daha fazla bilgi i\u00e7in [belgelere]( {more_info_url} ) bak\u0131n.", + "title": "\"configuration.yaml\" i\u00e7inde bulunan kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015f MQTT ayarlar\u0131" } }, "options": { "error": { "bad_birth": "Ge\u00e7ersiz do\u011fum konusu.", - "bad_will": "Ge\u00e7ersiz olacak konu.", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "bad_certificate": "CA sertifikas\u0131 ge\u00e7ersiz", + "bad_client_cert": "Ge\u00e7ersiz m\u00fc\u015fteri sertifikas\u0131, PEM kodlu bir dosyan\u0131n sa\u011fland\u0131\u011f\u0131ndan emin olun", + "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel ge\u00e7erli bir \u00e7ift de\u011fil", + "bad_client_key": "Ge\u00e7ersiz \u00f6zel anahtar, PEM kodlu bir dosyan\u0131n parola olmadan sa\u011fland\u0131\u011f\u0131ndan emin olun", + "bad_discovery_prefix": "Ge\u00e7ersiz ke\u015fif \u00f6neki", + "bad_will": "Ge\u00e7ersiz olacak konu", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_inclusion": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar birlikte yap\u0131land\u0131r\u0131lmal\u0131d\u0131r" }, "step": { "broker": { "data": { + "advanced_options": "Geli\u015fmi\u015f se\u00e7enekler", "broker": "Broker", + "certificate": "\u00d6zel CA sertifika dosyas\u0131 y\u00fckleyin", + "client_cert": "\u0130stemci sertifika dosyas\u0131n\u0131 y\u00fckleyin", + "client_id": "M\u00fc\u015fteri Kimli\u011fi (rastgele olu\u015fturulmu\u015f olana kadar bo\u015f b\u0131rak\u0131n)", + "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131 y\u00fckleyin", + "keepalive": "Canl\u0131 tutma mesajlar\u0131 g\u00f6nderme aras\u0131ndaki s\u00fcre", "password": "Parola", "port": "Port", + "protocol": "MQTT protokol\u00fc", + "set_ca_cert": "Arac\u0131 sertifikas\u0131 do\u011frulama", + "set_client_cert": "\u0130stemci sertifikas\u0131 kullan", + "tls_insecure": "Arac\u0131 sertifika do\u011frulamas\u0131n\u0131 yoksay", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin.", @@ -80,13 +117,14 @@ "birth_retain": "Do\u011fum mesaj\u0131 saklama", "birth_topic": "Do\u011fum mesaj\u0131 konusu", "discovery": "Ke\u015ffetmeyi etkinle\u015ftir", + "discovery_prefix": "Ke\u015fif \u00f6neki", "will_enable": "Etkinle\u015ftir iletisi", "will_payload": "\u0130leti y\u00fckl\u00fc", "will_qos": "QoS mesaj\u0131 g\u00f6nderecek", "will_retain": "Mesaj korunacak m\u0131", "will_topic": "Mesaj konusu olacak" }, - "description": "Ke\u015fif - Ke\u015fif etkinle\u015ftirilirse (\u00f6nerilir), Home Assistant, yap\u0131land\u0131rmalar\u0131n\u0131 MQTT arac\u0131s\u0131nda yay\u0131nlayan cihazlar\u0131 ve varl\u0131klar\u0131 otomatik olarak ke\u015ffeder. Ke\u015fif devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, t\u00fcm yap\u0131land\u0131rma manuel olarak yap\u0131lmal\u0131d\u0131r.\n Do\u011fum mesaj\u0131 - Do\u011fum mesaj\u0131, Home Assistant (yeniden) MQTT arac\u0131s\u0131na her ba\u011fland\u0131\u011f\u0131nda g\u00f6nderilir.\n Will mesaj\u0131 - Will mesaj\u0131, Home Assistant arac\u0131yla olan ba\u011flant\u0131s\u0131n\u0131 her kaybetti\u011finde, hem temizlik durumunda (\u00f6rn. Home Assistant kapan\u0131yor) hem de kirli bir durumda (\u00f6rn. ba\u011flant\u0131y\u0131 kes.", + "description": "Ke\u015fif - Ke\u015fif etkinle\u015ftirilirse (\u00f6nerilir), Home Assistant, yap\u0131land\u0131rmalar\u0131n\u0131 MQTT arac\u0131s\u0131nda yay\u0131nlayan cihazlar\u0131 ve varl\u0131klar\u0131 otomatik olarak ke\u015ffeder. Ke\u015fif devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, t\u00fcm yap\u0131land\u0131rma manuel olarak yap\u0131lmal\u0131d\u0131r.\n Ke\u015fif \u00f6neki - Otomatik ke\u015fif i\u00e7in bir yap\u0131land\u0131rma konusunun ba\u015flamas\u0131 gereken \u00f6nek.\n Do\u011fum mesaj\u0131 - Do\u011fum mesaj\u0131, Home Assistant (yeniden) MQTT arac\u0131s\u0131na her ba\u011fland\u0131\u011f\u0131nda g\u00f6nderilir.\n Will mesaj\u0131 - Will mesaj\u0131, Home Assistant arac\u0131yla olan ba\u011flant\u0131s\u0131n\u0131 her kaybetti\u011finde, hem temizlik durumunda (\u00f6rn. Home Assistant kapan\u0131yor) hem de kirli bir durumda (\u00f6rn. ba\u011flant\u0131y\u0131 kes.", "title": "MQTT se\u00e7enekleri" } } diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json index b684595b170..ec09eef166c 100644 --- a/homeassistant/components/mqtt/translations/uk.json +++ b/homeassistant/components/mqtt/translations/uk.json @@ -10,7 +10,6 @@ "broker": { "data": { "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", - "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" diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json index f897bee3c9b..245728aa64c 100644 --- a/homeassistant/components/mqtt/translations/zh-Hans.json +++ b/homeassistant/components/mqtt/translations/zh-Hans.json @@ -5,16 +5,19 @@ "single_instance_allowed": "\u8be5\u96c6\u6210\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002\u82e5\u8981\u91cd\u65b0\u914d\u7f6e\uff0c\u8bf7\u5148\u5220\u9664\u65e7\u96c6\u6210\u3002" }, "error": { + "bad_ws_headers": "\u63d0\u4f9b\u4ee5JSON\u5bf9\u8c61\u5f62\u5f0f\u7684\u6709\u6548HTTP headers", "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u3002" }, "step": { "broker": { "data": { "broker": "\u670d\u52a1\u5668", - "discovery": "\u542f\u7528\u53d1\u73b0", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", - "username": "\u7528\u6237\u540d" + "transport": "MQTT\u4f20\u8f93", + "username": "\u7528\u6237\u540d", + "ws_headers": "WebSocket headers \uff08JSON\u683c\u5f0f\uff09", + "ws_path": "WebSocket \u8def\u5f84" }, "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002" }, @@ -53,6 +56,7 @@ "error": { "bad_birth": "\u51fa\u751f\u6d88\u606f\u4e3b\u9898\u65e0\u6548", "bad_will": "\u9057\u5631\u6d88\u606f\u4e3b\u9898\u65e0\u6548", + "bad_ws_headers": "\u63d0\u4f9b\u4ee5JSON\u5bf9\u8c61\u5f62\u5f0f\u7684\u6709\u6548HTTP headers", "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { @@ -61,7 +65,10 @@ "broker": "\u670d\u52a1\u5668", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", - "username": "\u7528\u6237\u540d" + "transport": "MQTT\u4f20\u8f93", + "username": "\u7528\u6237\u540d", + "ws_headers": "WebSocket headers \uff08JSON\u683c\u5f0f\uff09", + "ws_path": "WebSocket\u5730\u5740" }, "description": "\u8bf7\u8f93\u5165 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002", "title": "\u670d\u52a1\u5668\u9009\u9879" diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index c7d08ce7482..110eac492c5 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -7,16 +7,35 @@ "error": { "bad_birth": "Birth \u4e3b\u984c\u7121\u6548", "bad_certificate": "CA \u8a8d\u8b49\u7121\u6548", - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "bad_client_cert": "\u5ba2\u6236\u7aef\u6191\u8b49\u7121\u6548\u3001\u8acb\u78ba\u5b9a\u63d0\u4f9b\u70ba PEM \u7de8\u78bc\u6a94\u6848", + "bad_client_cert_key": "\u5ba2\u6236\u7aef\u6191\u8b49\u8207\u79c1\u9470\u4e0d\u7b26\u5408", + "bad_client_key": "\u79c1\u9470\u7121\u6548\u3001\u8acb\u78ba\u5b9a\u63d0\u4f9b\u70ba PEM \u7de8\u78bc\u6a94\u6848\u4e26\u4e0d\u5177\u5bc6\u78bc", + "bad_discovery_prefix": "\u641c\u7d22\u4e3b\u984c prefix \u7121\u6548", + "bad_will": "Will \u4e3b\u984c\u7121\u6548", + "bad_ws_headers": "\u63d0\u4f9b\u6709\u6548\u7684 HTTP header \u4f5c\u70ba JSON \u7269\u4ef6", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_inclusion": "\u5ba2\u6236\u7aef\u6191\u8b49\u8207\u79c1\u9470\u5fc5\u9808\u4e00\u8d77\u8a2d\u5b9a" }, "step": { "broker": { "data": { + "advanced_options": "\u9032\u968e\u8a2d\u5b9a", "broker": "Broker", - "discovery": "\u958b\u555f\u641c\u5c0b", + "certificate": "\u4e0a\u50b3\u81ea\u8a02 CA \u6191\u8b49\u6a94\u6848", + "client_cert": "\u4e0a\u50b3\u5ba2\u6236\u7aef\u6191\u8b49\u6a94\u6848", + "client_id": "\u5ba2\u6236\u7aef ID (\u4fdd\u6301\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f)", + "client_key": "\u4e0a\u50b3\u79c1\u9470\u6a94\u6848", + "keepalive": "\u50b3\u9001\u4fdd\u6301\u6d3b\u52d5\u8a0a\u606f\u9593\u9694\u6642\u9593", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" + "protocol": "MQTT \u901a\u8a0a\u5354\u5b9a", + "set_ca_cert": "\u4ee3\u7406\u4f3a\u670d\u5668\u6191\u8b49\u9a57\u8b49", + "set_client_cert": "\u4f7f\u7528\u5ba2\u6236\u7aef\u6191\u8b49", + "tls_insecure": "\u5ffd\u7565\u4ee3\u7406\u4f3a\u670d\u5668\u6191\u8b49\u9a57\u8b49", + "transport": "MQTT \u50b3\u8f38", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "ws_headers": "JSON \u683c\u5f0f\u7684 WebSocket header", + "ws_path": "WebSocket \u8def\u5f91" }, "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002" }, @@ -55,21 +74,45 @@ "deprecated_yaml": { "description": "\u65bc\u5e73\u53f0\u9375 `{platform}` \u5167\u627e\u5230\u624b\u52d5\u8a2d\u5b9a\u4e4b MQTT {platform}\u3002\n\n\u8acb\u5c07\u8a2d\u5b9a\u79fb\u52d5\u81f3 `mqtt` \u6574\u5408\u9375\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002\u8acb\u53c3\u95b1 [\u6587\u4ef6]({more_info_url}) \u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a\u3002", "title": "\u624b\u52d5\u8a2d\u5b9a\u4e4b MQTT {platform} \u6709\u4e9b\u554f\u984c\u9700\u8981\u8655\u7406" + }, + "deprecated_yaml_broker_settings": { + "description": "\u65bc `configuration.yaml` \u4e2d\u767c\u73fe\u5df2\u7d93\u9077\u79fb\u81f3 MQTT \u8a2d\u5b9a\u5be6\u9ad4\u4e4b\u8a2d\u5b9a\u3001\u5c07\u8986\u84cb `configuration.yaml` \u4e2d\u7684\u8a2d\u5b9a\uff1a`{deprecated_settings}`\n\n\u8acb\u79fb\u9664 `configuration.yaml` \u4e2d\u7684\u8a2d\u5b9a\u3001\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002\u53c3\u95b1 [\u6587\u4ef6]({more_info_url}) \u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a\u3002", + "title": "\u65bc `configuration.yaml` \u4e2d\u767c\u73fe\u5df2\u505c\u7528\u7684 MQTT \u8a2d\u5b9a" } }, "options": { "error": { - "bad_birth": "Birth \u4e3b\u984c\u7121\u6548\u3002", - "bad_will": "Will \u4e3b\u984c\u7121\u6548\u3002", - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "bad_birth": "Birth \u4e3b\u984c\u7121\u6548", + "bad_certificate": "CA \u8a8d\u8b49\u7121\u6548", + "bad_client_cert": "\u5ba2\u6236\u7aef\u6191\u8b49\u7121\u6548\u3001\u8acb\u78ba\u5b9a\u63d0\u4f9b\u70ba PEM \u7de8\u78bc\u6a94\u6848", + "bad_client_cert_key": "\u5ba2\u6236\u7aef\u6191\u8b49\u8207\u79c1\u9470\u4e0d\u7b26\u5408", + "bad_client_key": "\u79c1\u9470\u7121\u6548\u3001\u8acb\u78ba\u5b9a\u63d0\u4f9b\u70ba PEM \u7de8\u78bc\u6a94\u6848\u4e26\u4e0d\u5177\u5bc6\u78bc", + "bad_discovery_prefix": "\u641c\u7d22\u4e3b\u984c prefix \u7121\u6548", + "bad_will": "Will \u4e3b\u984c\u7121\u6548", + "bad_ws_headers": "\u63d0\u4f9b\u6709\u6548\u7684 HTTP header \u4f5c\u70ba JSON \u7269\u4ef6", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_inclusion": "\u5ba2\u6236\u7aef\u6191\u8b49\u8207\u79c1\u9470\u5fc5\u9808\u4e00\u8d77\u8a2d\u5b9a" }, "step": { "broker": { "data": { + "advanced_options": "\u9032\u968e\u8a2d\u5b9a", "broker": "Broker", + "certificate": "\u4e0a\u50b3\u81ea\u8a02 CA \u6191\u8b49\u6a94\u6848", + "client_cert": "\u4e0a\u50b3\u5ba2\u6236\u7aef\u6191\u8b49\u6a94\u6848", + "client_id": "\u5ba2\u6236\u7aef ID (\u4fdd\u6301\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f)", + "client_key": "\u4e0a\u50b3\u79c1\u9470\u6a94\u6848", + "keepalive": "\u50b3\u9001\u4fdd\u6301\u6d3b\u52d5\u8a0a\u606f\u9593\u9694\u6642\u9593", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" + "protocol": "MQTT \u901a\u8a0a\u5354\u5b9a", + "set_ca_cert": "\u4ee3\u7406\u4f3a\u670d\u5668\u6191\u8b49\u9a57\u8b49", + "set_client_cert": "\u4f7f\u7528\u5ba2\u6236\u7aef\u6191\u8b49", + "tls_insecure": "\u5ffd\u7565\u4ee3\u7406\u4f3a\u670d\u5668\u6191\u8b49\u9a57\u8b49", + "transport": "MQTT \u50b3\u8f38", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "ws_headers": "JSON \u683c\u5f0f\u7684 WebSocket header", + "ws_path": "WebSocket \u8def\u5f91" }, "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002", "title": "Broker \u9078\u9805" @@ -81,14 +124,15 @@ "birth_qos": "Birth \u8a0a\u606f QoS", "birth_retain": "Birth \u8a0a\u606f Retain", "birth_topic": "Birth \u8a0a\u606f\u4e3b\u984c", - "discovery": "\u958b\u555f\u63a2\u7d22", + "discovery": "\u958b\u555f\u641c\u5c0b", + "discovery_prefix": "\u63a2\u7d22 prefix", "will_enable": "\u958b\u555f Will \u8a0a\u606f", "will_payload": "Will \u8a0a\u606f payload", "will_qos": "Will \u8a0a\u606f QoS", "will_retain": "Will \u8a0a\u606f Retain", "will_topic": "Will \u8a0a\u606f\u4e3b\u984c" }, - "description": "Discovery - \u5047\u5982\u641c\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT Broker\u3002\u5047\u5982\u641c\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT Broker \u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea Broker \u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", + "description": "Discovery - \u5047\u5982\u641c\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT \u4ee3\u7406\u4f3a\u670d\u5668\u3002\u5047\u5982\u641c\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\n\u63a2\u7d22 prefix - \u5fc5\u9808\u4f7f\u7528 prefix \u8a2d\u5b9a\u4e3b\u984c\u958b\u982d\u624d\u80fd\u88ab\u81ea\u52d5\u63a2\u7d22\u767c\u73fe\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT \u4ee3\u7406\u4f3a\u670d\u5668\u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea\u4ee3\u7406\u4f3a\u670d\u5668\u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", "title": "MQTT \u9078\u9805" } } diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 3530538122d..c10e539f8ec 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -1,21 +1,31 @@ """Offer MQTT listening automation rules.""" +from __future__ import annotations + +from collections.abc import Callable from contextlib import suppress import logging +from typing import Any import voluptuous as vol from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_loads -from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.template import Template +from homeassistant.helpers.trigger import TriggerActionType, TriggerData, TriggerInfo +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .. import mqtt from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING, DEFAULT_QOS - -# mypy: allow-untyped-defs - +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PayloadSentinel, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { @@ -40,43 +50,37 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = trigger_info["trigger_data"] - topic = config[CONF_TOPIC] - wanted_payload = config.get(CONF_PAYLOAD) - value_template = config.get(CONF_VALUE_TEMPLATE) - encoding = config[CONF_ENCODING] or None - qos = config[CONF_QOS] + trigger_data: TriggerData = trigger_info["trigger_data"] + command_template: Callable[ + [PublishPayloadType, TemplateVarsType], PublishPayloadType + ] = MqttCommandTemplate(config.get(CONF_PAYLOAD), hass=hass).async_render + value_template: Callable[[ReceivePayloadType, str], ReceivePayloadType] + value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), hass=hass + ).async_render_with_possible_json_value + encoding: str | None = config[CONF_ENCODING] or None + qos: int = config[CONF_QOS] job = HassJob(action) - variables = None + variables: TemplateVarsType | None = None if trigger_info: variables = trigger_info.get("variables") - template.attach(hass, wanted_payload) - if wanted_payload: - wanted_payload = wanted_payload.async_render( - variables, limited=True, parse_result=False - ) + wanted_payload = command_template(None, variables) - template.attach(hass, topic) - if isinstance(topic, template.Template): - topic = topic.async_render(variables, limited=True, parse_result=False) - topic = mqtt.util.valid_subscribe_topic(topic) - - template.attach(hass, value_template) + topic_template: Template = config[CONF_TOPIC] + topic_template.hass = hass + topic = topic_template.async_render(variables, limited=True, parse_result=False) + mqtt.util.valid_subscribe_topic(topic) @callback - def mqtt_automation_listener(mqttmsg): + def mqtt_automation_listener(mqttmsg: ReceiveMessage) -> None: """Listen for MQTT messages.""" - payload = mqttmsg.payload - - if value_template is not None: - payload = value_template.async_render_with_possible_json_value( - payload, - error_value=None, - ) - - if wanted_payload is None or wanted_payload == payload: - data = { + if wanted_payload is None or ( + (payload := value_template(mqttmsg.payload, PayloadSentinel.DEFAULT)) + and payload is not PayloadSentinel.DEFAULT + and wanted_payload == payload + ): + data: dict[str, Any] = { **trigger_data, "platform": "mqtt", "topic": mqttmsg.topic, diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index abad1cdb2ff..874f6024a37 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -172,14 +172,22 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): ) return - json_payload = {} + json_payload: Any | dict = {} try: json_payload = json_loads(payload) - _LOGGER.debug( - "JSON payload detected after processing payload '%s' on topic %s", - json_payload, - msg.topic, - ) + if isinstance(json_payload, dict): + _LOGGER.debug( + "JSON payload detected after processing payload '%s' on topic %s", + json_payload, + msg.topic, + ) + else: + _LOGGER.debug( + "Non-dictionary JSON payload detected after processing payload '%s' on topic %s", + payload, + msg.topic, + ) + json_payload = {"installed_version": payload} except JSON_DECODE_EXCEPTIONS: _LOGGER.debug( "No valid (JSON) payload detected after processing payload '%s' on topic %s", @@ -253,9 +261,9 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @property - def supported_features(self) -> int: + def supported_features(self) -> UpdateEntityFeature: """Return the list of supported features.""" - support = 0 + support = UpdateEntityFeature(0) if self._config.get(CONF_COMMAND_TOPIC) is not None: support |= UpdateEntityFeature.INSTALL diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 0b2d10977aa..97bb120f842 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -9,7 +9,6 @@ from typing import Any import voluptuous as vol -from homeassistant.const import CONF_PAYLOAD from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType @@ -32,6 +31,8 @@ from .models import MqttData TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" +_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) + def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None: """Return true when the MQTT config entry is enabled.""" @@ -96,7 +97,7 @@ def valid_subscribe_topic(topic: Any) -> str: def valid_subscribe_topic_template(value: Any) -> template.Template: """Validate either a jinja2 template or a valid MQTT subscription topic.""" - tpl = template.Template(value) + tpl = cv.template(value) if tpl.is_static: valid_subscribe_topic(value) @@ -112,24 +113,38 @@ def valid_publish_topic(topic: Any) -> str: return validated_topic -_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) +def valid_qos_schema(qos: Any) -> int: + """Validate that QOS value is valid.""" + validated_qos: int = _VALID_QOS_SCHEMA(qos) + return validated_qos -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + +_MQTT_WILL_BIRTH_SCHEMA = vol.Schema( { vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Required(ATTR_PAYLOAD): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True, ) +def valid_birth_will(config: ConfigType) -> ConfigType: + """Validate a birth or will configuration and required topic/payload.""" + if config: + config = _MQTT_WILL_BIRTH_SCHEMA(config) + return config + + def get_mqtt_data(hass: HomeAssistant, ensure_exists: bool = False) -> MqttData: """Return typed MqttData from hass.data[DATA_MQTT].""" + mqtt_data: MqttData if ensure_exists: - return hass.data.setdefault(DATA_MQTT, MqttData()) - return hass.data[DATA_MQTT] + mqtt_data = hass.data.setdefault(DATA_MQTT, MqttData()) + return mqtt_data + mqtt_data = hass.data[DATA_MQTT] + return mqtt_data async def async_create_certificate_temp_files( @@ -180,7 +195,7 @@ def migrate_certificate_file_to_content(file_name_or_auto: str) -> str | None: if file_name_or_auto == "auto": return "auto" try: - with open(file_name_or_auto, encoding=DEFAULT_ENCODING) as certiticate_file: - return certiticate_file.read() + with open(file_name_or_auto, encoding=DEFAULT_ENCODING) as certificate_file: + return certificate_file.read() except OSError: return None diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index abab55c632c..60f8d7a7d45 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from ..mixins import async_setup_entry_helper, async_setup_platform_helper +from ..mixins import async_setup_entry_helper, warn_for_legacy_schema from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import ( DISCOVERY_SCHEMA_LEGACY, @@ -27,35 +27,39 @@ from .schema_state import ( ) -def validate_mqtt_vacuum_discovery(value): +def validate_mqtt_vacuum_discovery(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum schema.""" schemas = {LEGACY: DISCOVERY_SCHEMA_LEGACY, STATE: DISCOVERY_SCHEMA_STATE} - return schemas[value[CONF_SCHEMA]](value) + config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) + return config -# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 -def validate_mqtt_vacuum(value): +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +def validate_mqtt_vacuum(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum schema (deprecated).""" schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} - return schemas[value[CONF_SCHEMA]](value) + config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) + return config -def validate_mqtt_vacuum_modern(value): +def validate_mqtt_vacuum_modern(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum modern schema.""" schemas = { LEGACY: PLATFORM_SCHEMA_LEGACY_MODERN, STATE: PLATFORM_SCHEMA_STATE_MODERN, } - return schemas[value[CONF_SCHEMA]](value) + config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) + return config DISCOVERY_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_discovery ) -# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( - MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum + warn_for_legacy_schema(vacuum.DOMAIN), ) PLATFORM_SCHEMA_MODERN = vol.All( @@ -63,23 +67,6 @@ PLATFORM_SCHEMA_MODERN = vol.All( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up MQTT vacuum through configuration.yaml.""" - # Deprecated in HA Core 2022.6 - await async_setup_platform_helper( - hass, - vacuum.DOMAIN, - discovery_info or config, - async_add_entities, - _async_setup_entity, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -96,8 +83,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT vacuum.""" setup_entity = { diff --git a/homeassistant/components/mqtt/vacuum/schema.py b/homeassistant/components/mqtt/vacuum/schema.py index 949b5cede9c..78175f61255 100644 --- a/homeassistant/components/mqtt/vacuum/schema.py +++ b/homeassistant/components/mqtt/vacuum/schema.py @@ -1,7 +1,12 @@ """Shared schema code.""" +from __future__ import annotations + import voluptuous as vol -CONF_SCHEMA = "schema" +from homeassistant.components.vacuum import VacuumEntityFeature + +from ..const import CONF_SCHEMA + LEGACY = "legacy" STATE = "state" @@ -14,18 +19,23 @@ MQTT_VACUUM_SCHEMA = vol.Schema( ) -def services_to_strings(services, service_to_string): +def services_to_strings( + services: VacuumEntityFeature, + service_to_string: dict[VacuumEntityFeature, str], +) -> list[str]: """Convert SUPPORT_* service bitmask to list of service strings.""" - strings = [] - for service in service_to_string: - if service & services: - strings.append(service_to_string[service]) - return strings + return [ + service_to_string[service] + for service in service_to_string + if service & services + ] -def strings_to_services(strings, string_to_service): +def strings_to_services( + strings: list[str], string_to_service: dict[str, VacuumEntityFeature] +) -> VacuumEntityFeature: """Convert service strings to SUPPORT_* service bitmask.""" - services = 0 + services = VacuumEntityFeature(0) for string in strings: services |= string_to_service[string] return services diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 09c4448fda7..c8f8afd70df 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,4 +1,9 @@ """Support for Legacy MQTT vacuum.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + import voluptuous as vol from homeassistant.components.vacuum import ( @@ -8,18 +13,26 @@ from homeassistant.components.vacuum import ( VacuumEntity, VacuumEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.json import json_dumps +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .. import subscription from ..config import MQTT_BASE_SCHEMA from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema -from ..models import MqttValueTemplate +from ..models import ( + MqttValueTemplate, + PayloadSentinel, + ReceiveMessage, + ReceivePayloadType, +) from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -147,7 +160,7 @@ PLATFORM_SCHEMA_LEGACY_MODERN = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 PLATFORM_SCHEMA_LEGACY = vol.All( cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_LEGACY_MODERN.schema), warn_for_legacy_schema(VACUUM_DOMAIN), @@ -158,9 +171,45 @@ DISCOVERY_SCHEMA_LEGACY = PLATFORM_SCHEMA_LEGACY_MODERN.extend( ) +_COMMANDS = { + VacuumEntityFeature.TURN_ON: { + "payload": CONF_PAYLOAD_TURN_ON, + "status": "Cleaning", + }, + VacuumEntityFeature.TURN_OFF: { + "payload": CONF_PAYLOAD_TURN_OFF, + "status": "Turning Off", + }, + VacuumEntityFeature.STOP: { + "payload": CONF_PAYLOAD_STOP, + "status": "Stopping the current task", + }, + VacuumEntityFeature.CLEAN_SPOT: { + "payload": CONF_PAYLOAD_CLEAN_SPOT, + "status": "Cleaning spot", + }, + VacuumEntityFeature.LOCATE: { + "payload": CONF_PAYLOAD_LOCATE, + "status": "Hi, I'm over here!", + }, + VacuumEntityFeature.PAUSE: { + "payload": CONF_PAYLOAD_START_PAUSE, + "status": "Pausing/Resuming cleaning...", + }, + VacuumEntityFeature.RETURN_HOME: { + "payload": CONF_PAYLOAD_RETURN_TO_BASE, + "status": "Returning home...", + }, +} + + async def async_setup_entity_legacy( - hass, config, async_add_entities, config_entry, discovery_data -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, +) -> None: """Set up a MQTT Vacuum Legacy.""" async_add_entities([MqttVacuum(hass, config, config_entry, discovery_data)]) @@ -171,30 +220,48 @@ class MqttVacuum(MqttEntity, VacuumEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _encoding: str | None + _qos: bool + _retain: bool + _payloads: dict[str, str] + _send_command_topic: str | None + _set_fan_speed_topic: str | None + _state_topics: dict[str, str | None] + _templates: dict[ + str, Callable[[ReceivePayloadType, PayloadSentinel], ReceivePayloadType] + ] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the vacuum.""" - self._cleaning = False + self._attr_battery_level = 0 + self._attr_is_on = False + self._attr_fan_speed = "unknown" + self._charging = False + self._cleaning = False self._docked = False - self._error = None - self._status = "Unknown" - self._battery_level = 0 - self._fan_speed = "unknown" - self._fan_speed_list = [] + self._error: str | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA_LEGACY - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" supported_feature_strings = config[CONF_SUPPORTED_FEATURES] - self._supported_features = strings_to_services( + self._attr_supported_features = strings_to_services( supported_feature_strings, STRING_TO_SERVICE ) - self._fan_speed_list = config[CONF_FAN_SPEED_LIST] + self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] self._qos = config[CONF_QOS] self._retain = config[CONF_RETAIN] self._encoding = config[CONF_ENCODING] or None @@ -204,7 +271,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) self._payloads = { - key: config.get(key) + key: config[key] for key in ( CONF_PAYLOAD_TURN_ON, CONF_PAYLOAD_TURN_OFF, @@ -227,7 +294,9 @@ class MqttVacuum(MqttEntity, VacuumEntity): ) } self._templates = { - key: config.get(key) + key: MqttValueTemplate( + config[key], entity=self + ).async_render_with_possible_json_value for key in ( CONF_BATTERY_LEVEL_TEMPLATE, CONF_CHARGING_TEMPLATE, @@ -236,89 +305,87 @@ class MqttVacuum(MqttEntity, VacuumEntity): CONF_ERROR_TEMPLATE, CONF_FAN_SPEED_TEMPLATE, ) + if key in config } - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - for tpl in self._templates.values(): - if tpl is not None: - tpl = MqttValueTemplate(tpl, entity=self) @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT message.""" if ( msg.topic == self._state_topics[CONF_BATTERY_LEVEL_TOPIC] - and self._templates[CONF_BATTERY_LEVEL_TEMPLATE] + and CONF_BATTERY_LEVEL_TEMPLATE in self._config ): - battery_level = self._templates[ - CONF_BATTERY_LEVEL_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if battery_level: - self._battery_level = int(battery_level) + battery_level = self._templates[CONF_BATTERY_LEVEL_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if battery_level and battery_level is not PayloadSentinel.DEFAULT: + self._attr_battery_level = max(0, min(100, int(battery_level))) if ( msg.topic == self._state_topics[CONF_CHARGING_TOPIC] - and self._templates[CONF_CHARGING_TEMPLATE] + and CONF_CHARGING_TEMPLATE in self._templates ): - charging = self._templates[ - CONF_CHARGING_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if charging: + charging = self._templates[CONF_CHARGING_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if charging and charging is not PayloadSentinel.DEFAULT: self._charging = cv.boolean(charging) if ( msg.topic == self._state_topics[CONF_CLEANING_TOPIC] - and self._templates[CONF_CLEANING_TEMPLATE] + and CONF_CLEANING_TEMPLATE in self._config ): - cleaning = self._templates[ - CONF_CLEANING_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if cleaning: - self._cleaning = cv.boolean(cleaning) + cleaning = self._templates[CONF_CLEANING_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if cleaning and cleaning is not PayloadSentinel.DEFAULT: + self._attr_is_on = cv.boolean(cleaning) if ( msg.topic == self._state_topics[CONF_DOCKED_TOPIC] - and self._templates[CONF_DOCKED_TEMPLATE] + and CONF_DOCKED_TEMPLATE in self._config ): - docked = self._templates[ - CONF_DOCKED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if docked: + docked = self._templates[CONF_DOCKED_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if docked and docked is not PayloadSentinel.DEFAULT: self._docked = cv.boolean(docked) if ( msg.topic == self._state_topics[CONF_ERROR_TOPIC] - and self._templates[CONF_ERROR_TEMPLATE] + and CONF_ERROR_TEMPLATE in self._config ): - error = self._templates[ - CONF_ERROR_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if error is not None: + error = self._templates[CONF_ERROR_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if error is not PayloadSentinel.DEFAULT: self._error = cv.string(error) if self._docked: if self._charging: - self._status = "Docked & Charging" + self._attr_status = "Docked & Charging" else: - self._status = "Docked" - elif self._cleaning: - self._status = "Cleaning" + self._attr_status = "Docked" + elif self.is_on: + self._attr_status = "Cleaning" elif self._error: - self._status = f"Error: {self._error}" + self._attr_status = f"Error: {self._error}" else: - self._status = "Stopped" + self._attr_status = "Stopped" if ( msg.topic == self._state_topics[CONF_FAN_SPEED_TOPIC] - and self._templates[CONF_FAN_SPEED_TEMPLATE] + and CONF_FAN_SPEED_TEMPLATE in self._config ): - fan_speed = self._templates[ - CONF_FAN_SPEED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if fan_speed: - self._fan_speed = fan_speed + fan_speed = self._templates[CONF_FAN_SPEED_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if fan_speed and fan_speed is not PayloadSentinel.DEFAULT: + self._attr_fan_speed = str(fan_speed) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -337,37 +404,12 @@ class MqttVacuum(MqttEntity, VacuumEntity): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def is_on(self): - """Return true if vacuum is on.""" - return self._cleaning - - @property - def status(self): - """Return a status string for the vacuum.""" - return self._status - - @property - def fan_speed(self): - """Return the status of the vacuum.""" - return self._fan_speed - - @property - def fan_speed_list(self): - """Return the status of the vacuum.""" - return self._fan_speed_list - - @property - def battery_level(self): - """Return the status of the vacuum.""" - return max(0, min(100, self._battery_level)) - - @property - def battery_icon(self): + def battery_icon(self) -> str: """Return the battery icon for the vacuum cleaner. No need to check VacuumEntityFeature.BATTERY, this won't be called if battery_level is None. @@ -376,121 +418,57 @@ class MqttVacuum(MqttEntity, VacuumEntity): battery_level=self.battery_level, charging=self._charging ) - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features + async def _async_publish_command(self, feature: VacuumEntityFeature) -> None: + """Check for a missing feature or command topic.""" - async def async_turn_on(self, **kwargs): - """Turn the vacuum on.""" - if self.supported_features & VacuumEntityFeature.TURN_ON == 0: + if self._command_topic is None or self.supported_features & feature == 0: return await self.async_publish( self._command_topic, - self._payloads[CONF_PAYLOAD_TURN_ON], - self._qos, - self._retain, - self._encoding, + self._payloads[_COMMANDS[feature]["payload"]], + qos=self._qos, + retain=self._retain, + encoding=self._encoding, ) - self._status = "Cleaning" + self._attr_status = _COMMANDS[feature]["status"] self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the vacuum on.""" + await self._async_publish_command(VacuumEntityFeature.TURN_ON) + + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the vacuum off.""" - if self.supported_features & VacuumEntityFeature.TURN_OFF == 0: - return None + await self._async_publish_command(VacuumEntityFeature.TURN_OFF) - await self.async_publish( - self._command_topic, - self._payloads[CONF_PAYLOAD_TURN_OFF], - self._qos, - self._retain, - self._encoding, - ) - self._status = "Turning Off" - self.async_write_ha_state() - - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum.""" - if self.supported_features & VacuumEntityFeature.STOP == 0: - return None + await self._async_publish_command(VacuumEntityFeature.STOP) - await self.async_publish( - self._command_topic, - self._payloads[CONF_PAYLOAD_STOP], - self._qos, - self._retain, - self._encoding, - ) - self._status = "Stopping the current task" - self.async_write_ha_state() - - async def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" - if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: - return None + await self._async_publish_command(VacuumEntityFeature.CLEAN_SPOT) - await self.async_publish( - self._command_topic, - self._payloads[CONF_PAYLOAD_CLEAN_SPOT], - self._qos, - self._retain, - self._encoding, - ) - self._status = "Cleaning spot" - self.async_write_ha_state() - - async def async_locate(self, **kwargs): + async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum (usually by playing a song).""" - if self.supported_features & VacuumEntityFeature.LOCATE == 0: - return None + await self._async_publish_command(VacuumEntityFeature.LOCATE) - await self.async_publish( - self._command_topic, - self._payloads[CONF_PAYLOAD_LOCATE], - self._qos, - self._retain, - self._encoding, - ) - self._status = "Hi, I'm over here!" - self.async_write_ha_state() - - async def async_start_pause(self, **kwargs): + async def async_start_pause(self, **kwargs: Any) -> None: """Start, pause or resume the cleaning task.""" - if self.supported_features & VacuumEntityFeature.PAUSE == 0: - return None + await self._async_publish_command(VacuumEntityFeature.PAUSE) - await self.async_publish( - self._command_topic, - self._payloads[CONF_PAYLOAD_START_PAUSE], - self._qos, - self._retain, - self._encoding, - ) - self._status = "Pausing/Resuming cleaning..." - self.async_write_ha_state() - - async def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: """Tell the vacuum to return to its dock.""" - if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: - return None + await self._async_publish_command(VacuumEntityFeature.RETURN_HOME) - await self.async_publish( - self._command_topic, - self._payloads[CONF_PAYLOAD_RETURN_TO_BASE], - self._qos, - self._retain, - self._encoding, - ) - self._status = "Returning home..." - self.async_write_ha_state() - - async def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if ( - self.supported_features & VacuumEntityFeature.FAN_SPEED == 0 - ) or fan_speed not in self._fan_speed_list: + self._set_fan_speed_topic is None + or (self.supported_features & VacuumEntityFeature.FAN_SPEED == 0) + or fan_speed not in self.fan_speed_list + ): return None await self.async_publish( @@ -500,25 +478,33 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._retain, self._encoding, ) - self._status = f"Setting fan to {fan_speed}..." + self._attr_status = f"Setting fan to {fan_speed}..." self.async_write_ha_state() - async def async_send_command(self, command, params=None, **kwargs): + async def async_send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to a vacuum cleaner.""" - if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: + if ( + self._send_command_topic is None + or self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0 + ): return if params: - message = {"command": command} + message: dict[str, Any] = {"command": command} message.update(params) - message = json_dumps(message) + message_payload = json_dumps(message) else: - message = command + message_payload = command await self.async_publish( self._send_command_topic, - message, + message_payload, self._qos, self._retain, self._encoding, ) - self._status = f"Sending command {message}..." + self._attr_status = f"Sending command {message_payload}..." self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 8dfaba80109..4e020c630a5 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -1,4 +1,8 @@ """Support for a State MQTT vacuum.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant.components.vacuum import ( @@ -11,15 +15,18 @@ from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_NAME, STATE_IDLE, STATE_PAUSED, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import json_dumps, json_loads +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .. import subscription from ..config import MQTT_BASE_SCHEMA @@ -32,11 +39,12 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema +from ..models import ReceiveMessage from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services -SERVICE_TO_STRING = { +SERVICE_TO_STRING: dict[VacuumEntityFeature, str] = { VacuumEntityFeature.START: "start", VacuumEntityFeature.PAUSE: "pause", VacuumEntityFeature.STOP: "stop", @@ -72,7 +80,7 @@ BATTERY = "battery_level" FAN_SPEED = "fan_speed" STATE = "state" -POSSIBLE_STATES = { +POSSIBLE_STATES: dict[str, str] = { STATE_IDLE: STATE_IDLE, STATE_DOCKED: STATE_DOCKED, STATE_ERROR: STATE_ERROR, @@ -104,6 +112,15 @@ DEFAULT_PAYLOAD_LOCATE = "locate" DEFAULT_PAYLOAD_START = "start" DEFAULT_PAYLOAD_PAUSE = "pause" +_FEATURE_PAYLOADS = { + VacuumEntityFeature.START: CONF_PAYLOAD_START, + VacuumEntityFeature.STOP: CONF_PAYLOAD_STOP, + VacuumEntityFeature.PAUSE: CONF_PAYLOAD_PAUSE, + VacuumEntityFeature.CLEAN_SPOT: CONF_PAYLOAD_CLEAN_SPOT, + VacuumEntityFeature.LOCATE: CONF_PAYLOAD_LOCATE, + VacuumEntityFeature.RETURN_HOME: CONF_PAYLOAD_RETURN_TO_BASE, +} + PLATFORM_SCHEMA_STATE_MODERN = ( MQTT_BASE_SCHEMA.extend( { @@ -137,7 +154,7 @@ PLATFORM_SCHEMA_STATE_MODERN = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 PLATFORM_SCHEMA_STATE = vol.All( cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_STATE_MODERN.schema), warn_for_legacy_schema(VACUUM_DOMAIN), @@ -147,8 +164,12 @@ DISCOVERY_SCHEMA_STATE = PLATFORM_SCHEMA_STATE_MODERN.extend({}, extra=vol.REMOV async def async_setup_entity_state( - hass, config, async_add_entities, config_entry, discovery_data -): + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, +) -> None: """Set up a State MQTT Vacuum.""" async_add_entities([MqttStateVacuum(hass, config, config_entry, discovery_data)]) @@ -159,25 +180,34 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _set_fan_speed_topic: str | None + _send_command_topic: str | None + _payloads: dict[str, str | None] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the vacuum.""" - self._state = None - self._state_attrs = {} - self._fan_speed_list = [] + self._state_attrs: dict[str, Any] = {} MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA_STATE - def _setup_from_config(self, config): - supported_feature_strings = config[CONF_SUPPORTED_FEATURES] - self._supported_features = strings_to_services( + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + supported_feature_strings: list[str] = config[CONF_SUPPORTED_FEATURES] + self._attr_supported_features = strings_to_services( supported_feature_strings, STRING_TO_SERVICE ) - self._fan_speed_list = config[CONF_FAN_SPEED_LIST] + self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] self._command_topic = config.get(CONF_COMMAND_TOPIC) self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) @@ -194,28 +224,34 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): ) } - def _prepare_subscribe_topics(self): + def _update_state_attributes(self, payload: dict[str, Any]) -> None: + """Update the entity state attributes.""" + self._state_attrs.update(payload) + self._attr_fan_speed = self._state_attrs.get(FAN_SPEED, 0) + self._attr_battery_level = max(0, min(100, self._state_attrs.get(BATTERY, 0))) + + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, Any] = {} @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle state MQTT message.""" - payload = json_loads(msg.payload) + payload: dict[str, Any] = json_loads(msg.payload) if STATE in payload and ( payload[STATE] in POSSIBLE_STATES or payload[STATE] is None ): - self._state = ( + self._attr_state = ( POSSIBLE_STATES[payload[STATE]] if payload[STATE] else None ) del payload[STATE] - self._state_attrs.update(payload) + self._update_state_attributes(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if self._config.get(CONF_STATE_TOPIC): + if state_topic := self._config.get(CONF_STATE_TOPIC): topics["state_position_topic"] = { - "topic": self._config.get(CONF_STATE_TOPIC), + "topic": state_topic, "msg_callback": state_message_received, "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, @@ -224,77 +260,56 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - @property - def state(self): - """Return state of vacuum.""" - return self._state + async def _async_publish_command(self, feature: VacuumEntityFeature) -> None: + """Check for a missing feature or command topic.""" + if self._command_topic is None or self.supported_features & feature == 0: + return - @property - def fan_speed(self): - """Return fan speed of the vacuum.""" - return self._state_attrs.get(FAN_SPEED, 0) + await self.async_publish( + self._command_topic, + self._payloads[_FEATURE_PAYLOADS[feature]], + qos=self._config[CONF_QOS], + retain=self._config[CONF_RETAIN], + encoding=self._config[CONF_ENCODING], + ) + self.async_write_ha_state() - @property - def fan_speed_list(self): - """Return fan speed list of the vacuum.""" - return self._fan_speed_list - - @property - def battery_level(self): - """Return battery level of the vacuum.""" - return max(0, min(100, self._state_attrs.get(BATTERY, 0))) - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - - async def async_start(self): + async def async_start(self) -> None: """Start the vacuum.""" - if self.supported_features & VacuumEntityFeature.START == 0: - return None - await self.async_publish( - self._command_topic, - self._config[CONF_PAYLOAD_START], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) + await self._async_publish_command(VacuumEntityFeature.START) - async def async_pause(self): + async def async_pause(self) -> None: """Pause the vacuum.""" - if self.supported_features & VacuumEntityFeature.PAUSE == 0: - return None - await self.async_publish( - self._command_topic, - self._config[CONF_PAYLOAD_PAUSE], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) + await self._async_publish_command(VacuumEntityFeature.PAUSE) - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum.""" - if self.supported_features & VacuumEntityFeature.STOP == 0: - return None - await self.async_publish( - self._command_topic, - self._config[CONF_PAYLOAD_STOP], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) + await self._async_publish_command(VacuumEntityFeature.STOP) - async def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: + """Tell the vacuum to return to its dock.""" + await self._async_publish_command(VacuumEntityFeature.RETURN_HOME) + + async def async_clean_spot(self, **kwargs: Any) -> None: + """Perform a spot clean-up.""" + await self._async_publish_command(VacuumEntityFeature.CLEAN_SPOT) + + async def async_locate(self, **kwargs: Any) -> None: + """Locate the vacuum (usually by playing a song).""" + await self._async_publish_command(VacuumEntityFeature.LOCATE) + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" - if (self.supported_features & VacuumEntityFeature.FAN_SPEED == 0) or ( - fan_speed not in self._fan_speed_list + if ( + self._set_fan_speed_topic is None + or (self.supported_features & VacuumEntityFeature.FAN_SPEED == 0) + or (fan_speed not in self.fan_speed_list) ): - return None + return await self.async_publish( self._set_fan_speed_topic, fan_speed, @@ -303,55 +318,27 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._config[CONF_ENCODING], ) - async def async_return_to_base(self, **kwargs): - """Tell the vacuum to return to its dock.""" - if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: - return None - await self.async_publish( - self._command_topic, - self._config[CONF_PAYLOAD_RETURN_TO_BASE], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) - - async def async_clean_spot(self, **kwargs): - """Perform a spot clean-up.""" - if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: - return None - await self.async_publish( - self._command_topic, - self._config[CONF_PAYLOAD_CLEAN_SPOT], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) - - async def async_locate(self, **kwargs): - """Locate the vacuum (usually by playing a song).""" - if self.supported_features & VacuumEntityFeature.LOCATE == 0: - return None - await self.async_publish( - self._command_topic, - self._config[CONF_PAYLOAD_LOCATE], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) - - async def async_send_command(self, command, params=None, **kwargs): + async def async_send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to a vacuum cleaner.""" - if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: - return None - if params: - message = {"command": command} + if ( + self._send_command_topic is None + or self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0 + ): + return + if isinstance(params, dict): + message: dict[str, Any] = {"command": command} message.update(params) - message = json_dumps(message) + payload = json_dumps(message) else: - message = command + payload = command await self.async_publish( self._send_command_topic, - message, + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index a6927048051..b1b52e42fce 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_NAME, CONF_TIMEOUT, + CONF_UNIQUE_ID, STATE_NOT_HOME, ) from homeassistant.core import HomeAssistant, callback @@ -42,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ).extend(mqtt.config.MQTT_RO_SCHEMA.schema) @@ -70,10 +72,11 @@ async def async_setup_platform( [ MQTTRoomSensor( config.get(CONF_NAME), - config.get(CONF_STATE_TOPIC), - config.get(CONF_DEVICE_ID), - config.get(CONF_TIMEOUT), - config.get(CONF_AWAY_TIMEOUT), + config[CONF_STATE_TOPIC], + config[CONF_DEVICE_ID], + config[CONF_TIMEOUT], + config[CONF_AWAY_TIMEOUT], + config.get(CONF_UNIQUE_ID), ) ] ) @@ -82,8 +85,18 @@ async def async_setup_platform( class MQTTRoomSensor(SensorEntity): """Representation of a room sensor that is updated via MQTT.""" - def __init__(self, name, state_topic, device_id, timeout, consider_home): + def __init__( + self, + name: str | None, + state_topic: str, + device_id: str, + timeout: int, + consider_home: int, + unique_id: str | None, + ) -> None: """Initialize the sensor.""" + self._attr_unique_id = unique_id + self._state = STATE_NOT_HOME self._name = name self._state_topic = f"{state_topic}/+" diff --git a/homeassistant/components/mullvad/translations/he.json b/homeassistant/components/mullvad/translations/he.json index 1551f5e6bb0..4b761e279f9 100644 --- a/homeassistant/components/mullvad/translations/he.json +++ b/homeassistant/components/mullvad/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/mullvad/translations/sk.json b/homeassistant/components/mullvad/translations/sk.json new file mode 100644 index 00000000000..1fa70439792 --- /dev/null +++ b/homeassistant/components/mullvad/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "description": "Nastavi\u0165 integr\u00e1ciu Mullvad VPN?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/sk.json b/homeassistant/components/mutesync/translations/sk.json new file mode 100644 index 00000000000..d4923a2f973 --- /dev/null +++ b/homeassistant/components/mutesync/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Povo\u013ete autentifik\u00e1ciu v m\u00fctesync Preferences > Authentication", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/bg.json b/homeassistant/components/myq/translations/bg.json index 728682f531e..df3fd63febb 100644 --- a/homeassistant/components/myq/translations/bg.json +++ b/homeassistant/components/myq/translations/bg.json @@ -2,9 +2,10 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/myq/translations/sk.json b/homeassistant/components/myq/translations/sk.json index 71a7aea5018..7f9c956aab3 100644 --- a/homeassistant/components/myq/translations/sk.json +++ b/homeassistant/components/myq/translations/sk.json @@ -1,10 +1,29 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Heslo pre {username} u\u017e nie je platn\u00e9.", + "title": "Znova overte svoj \u00fa\u010det MyQ" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripojte sa k MyQ Gateway" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 07d03c3debd..50ecf70f8fd 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -17,9 +16,9 @@ from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload SENSORS = { - "S_DOOR": "door", + "S_DOOR": BinarySensorDeviceClass.DOOR, "S_MOTION": BinarySensorDeviceClass.MOTION, - "S_SMOKE": "smoke", + "S_SMOKE": BinarySensorDeviceClass.SMOKE, "S_SPRINKLER": BinarySensorDeviceClass.SAFETY, "S_WATER_LEAK": BinarySensorDeviceClass.SAFETY, "S_SOUND": BinarySensorDeviceClass.SOUND, @@ -66,10 +65,7 @@ class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity return self._values.get(self.value_type) == STATE_ON @property - def device_class(self) -> str | None: + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this sensor, from DEVICE_CLASSES.""" pres = self.gateway.const.Presentation - device_class = SENSORS.get(pres(self.child_type).name) - if device_class in DEVICE_CLASSES: - return device_class - return None + return SENSORS.get(pres(self.child_type).name) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 44468d8db4c..38077768f2e 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -77,9 +77,9 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): _attr_hvac_modes = OPERATION_LIST @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" - features = 0 + features = ClimateEntityFeature(0) set_req = self.gateway.const.SetReq if set_req.V_HVAC_SPEED in self._values: features = features | ClimateEntityFeature.FAN_MODE diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 94ba8b80c2f..23c07adc9fa 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -34,7 +34,6 @@ "invalid_serial": "Port s\u00e8rie inv\u00e0lid", "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", - "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "not_a_number": "Introdueix un n\u00famero", "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535", "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos", diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index 2ab3da62431..5c477806600 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -34,7 +34,6 @@ "invalid_serial": "Ung\u00fcltiger Serieller Port", "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema", "invalid_version": "Ung\u00fcltige MySensors Version", - "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", "not_a_number": "Bitte eine Nummer eingeben", "port_out_of_range": "Die Portnummer muss mindestens 1 und darf h\u00f6chstens 65535 sein", "same_topic": "Themen zum Abonnieren und Ver\u00f6ffentlichen sind gleich", diff --git a/homeassistant/components/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index a761f148c27..f90d3770f7a 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -34,7 +34,6 @@ "invalid_serial": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1", "invalid_subscribe_topic": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", "invalid_version": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 MySensors", - "mqtt_required": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 MQTT \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "not_a_number": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc", "port_out_of_range": "\u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd 1 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03c0\u03bf\u03bb\u03cd 65535", "same_topic": "\u03a4\u03b1 \u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b7\u03bc\u03bf\u03c3\u03af\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b1 \u03af\u03b4\u03b9\u03b1", diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 081ae3a2b95..b85a28fb7d3 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -34,7 +34,6 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", - "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "same_topic": "Subscribe and publish topics are the same", diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 62010958d67..03619e114bf 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -34,7 +34,6 @@ "invalid_serial": "Puerto serie no v\u00e1lido", "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", - "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "not_a_number": "Por favor, introduce un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser al menos 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index a1bcacc1852..06cdc0e74a1 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -34,7 +34,6 @@ "invalid_serial": "Sobimatu jadaport", "invalid_subscribe_topic": "Kehtetu tellimisteema", "invalid_version": "Sobimatu MySensors versioon", - "mqtt_required": "MQTT sidumine on loomata", "not_a_number": "Sisesta number", "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535", "same_topic": "Tellimise ja avaldamise teemad kattuvad", diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index 0c691e64f2f..96724b4d5a9 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -34,7 +34,6 @@ "invalid_serial": "Port s\u00e9rie non valide", "invalid_subscribe_topic": "Sujet d'abonnement non valide", "invalid_version": "Version de MySensors non valide", - "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "not_a_number": "Veuillez saisir un nombre", "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535", "same_topic": "Les sujets de souscription et de publication sont identiques", diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json index 9787081e32e..f0a12592138 100644 --- a/homeassistant/components/mysensors/translations/he.json +++ b/homeassistant/components/mysensors/translations/he.json @@ -11,7 +11,6 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "mqtt_required": "\u05e9\u05d9\u05dc\u05d5\u05d1 MQTT \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 5253dd7b427..4a921724dd3 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -34,7 +34,6 @@ "invalid_serial": "\u00c9rv\u00e9nytelen soros port", "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si (subscribe) topik", "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", - "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", "not_a_number": "K\u00e9rem, sz\u00e1mot adjon meg", "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index 67a7469b48a..e5776c23316 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -34,7 +34,6 @@ "invalid_serial": "Port serial tidak valid", "invalid_subscribe_topic": "Topik langganan tidak valid", "invalid_version": "Versi MySensors tidak valid", - "mqtt_required": "Integrasi MQTT belum disiapkan", "not_a_number": "Masukkan angka", "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", "same_topic": "Topik subscribe dan publish sama", diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 0f4a2746790..b7063bb5e47 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -34,7 +34,6 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", - "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", "not_a_number": "Digita un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index fd2294ab820..689e7041f6d 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -34,7 +34,6 @@ "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", - "mqtt_required": "MQTT\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", diff --git a/homeassistant/components/mysensors/translations/ko.json b/homeassistant/components/mysensors/translations/ko.json index 4d5be0f44e3..7a6f3e856a7 100644 --- a/homeassistant/components/mysensors/translations/ko.json +++ b/homeassistant/components/mysensors/translations/ko.json @@ -33,7 +33,6 @@ "invalid_serial": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_subscribe_topic": "\uad6c\ub3c5 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_version": "MySensors \ubc84\uc804\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "mqtt_required": "MQTT \ud1b5\ud569\uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "not_a_number": "\uc22b\uc790\ub85c \uc785\ub825\ud574\uc8fc\uc138\uc694", "port_out_of_range": "\ud3ec\ud2b8 \ubc88\ud638\ub294 1 \uc774\uc0c1 65535 \uc774\ud558\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", "same_topic": "\uad6c\ub3c5 \ubc0f \ubc1c\ud589 \ud1a0\ud53d\uc740 \ub3d9\uc77c\ud569\ub2c8\ub2e4", diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 44bd06022b9..ba2eecff5d8 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -34,7 +34,6 @@ "invalid_serial": "Ongeldige seri\u00eble poort", "invalid_subscribe_topic": "Ongeldig abonneer topic", "invalid_version": "Ongeldige MySensors-versie", - "mqtt_required": "De MQTT integratie is niet ingesteld", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", "same_topic": "De topics abonneren en publiceren zijn hetzelfde", @@ -44,9 +43,9 @@ "gw_mqtt": { "data": { "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)", - "retain": "mqtt behouden", + "retain": "MQTT retentie", "topic_in_prefix": "prefix voor inkomende topics (topic_in_prefix)", - "topic_out_prefix": "prefix voor uitgaande topics (topic_out_prefix)", + "topic_out_prefix": "voorvoegsel voor uitgaande topics (topic_out_prefix)", "version": "MySensors-versie" }, "description": "MQTT-gateway instellen" diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 1e45899d981..0f119f61028 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -34,7 +34,6 @@ "invalid_serial": "Ugyldig serieport", "invalid_subscribe_topic": "Ugyldig abonnementsemne", "invalid_version": "Ugyldig MySensors-versjon", - "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "not_a_number": "Vennligst skriv inn et nummer", "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535", "same_topic": "Abonner og publiser emner er de samme", diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index ef473a6aff5..689bc021e89 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -34,7 +34,6 @@ "invalid_serial": "Nieprawid\u0142owy port szeregowy", "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", "invalid_version": "Nieprawid\u0142owa wersja MySensors", - "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "not_a_number": "Prosz\u0119 wpisa\u0107 numer", "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535", "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same", diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json index 29b8a9e9372..ea76d19f358 100644 --- a/homeassistant/components/mysensors/translations/pt-BR.json +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -34,7 +34,6 @@ "invalid_serial": "Porta serial inv\u00e1lida", "invalid_subscribe_topic": "T\u00f3pico de inscri\u00e7\u00e3o inv\u00e1lido", "invalid_version": "Vers\u00e3o MySensors inv\u00e1lida", - "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", "not_a_number": "Por favor, digite um n\u00famero", "port_out_of_range": "O n\u00famero da porta deve ser no m\u00ednimo 1 e no m\u00e1ximo 65535", "same_topic": "Subscrever e publicar t\u00f3picos s\u00e3o os mesmos", diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index 8d24debaf33..eaac7b9230e 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -34,7 +34,6 @@ "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.", "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.", - "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.", "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.", "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.", diff --git a/homeassistant/components/mysensors/translations/sk.json b/homeassistant/components/mysensors/translations/sk.json index 2c3ed1dd930..b73b76616e6 100644 --- a/homeassistant/components/mysensors/translations/sk.json +++ b/homeassistant/components/mysensors/translations/sk.json @@ -1,10 +1,72 @@ { "config": { "abort": { - "invalid_auth": "Neplatn\u00e9 overenie" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "duplicate_persistence_file": "Perzistentn\u00fd s\u00fabor sa u\u017e pou\u017e\u00edva", + "duplicate_topic": "T\u00e9ma sa u\u017e pou\u017e\u00edva", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_device": "Neplatn\u00e9 zariadenie", + "invalid_ip": "Neplatn\u00e1 IP adresa", + "invalid_port": "Neplatn\u00e9 \u010d\u00edslo portu", + "invalid_serial": "Neplatn\u00fd s\u00e9riov\u00fd port", + "invalid_version": "Neplatn\u00e1 verzia MySensors", + "mqtt_required": "Integr\u00e1cia MQTT nie je nastaven\u00e1", + "not_a_number": "Zadajte \u010d\u00edslo", + "port_out_of_range": "\u010c\u00edslo portu mus\u00ed by\u0165 minim\u00e1lne 1 a maxim\u00e1lne 65535", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "duplicate_persistence_file": "Perzistentn\u00fd s\u00fabor sa u\u017e pou\u017e\u00edva", + "duplicate_topic": "T\u00e9ma sa u\u017e pou\u017e\u00edva", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_device": "Neplatn\u00e9 zariadenie", + "invalid_ip": "Neplatn\u00e1 IP adresa", + "invalid_port": "Neplatn\u00e9 \u010d\u00edslo portu", + "invalid_serial": "Neplatn\u00fd s\u00e9riov\u00fd port", + "invalid_version": "Neplatn\u00e1 verzia MySensors", + "not_a_number": "Zadajte \u010d\u00edslo", + "port_out_of_range": "\u010c\u00edslo portu mus\u00ed by\u0165 minim\u00e1lne 1 a maxim\u00e1lne 65535", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "gw_mqtt": { + "data": { + "version": "Verzia MySensors" + }, + "description": "Nastavenie br\u00e1ny MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "prenosov\u00e1 r\u00fdchlos\u0165", + "device": "S\u00e9riov\u00fd port", + "version": "Verzia MySensors" + } + }, + "gw_tcp": { + "data": { + "device": "IP adresa br\u00e1ny", + "tcp_port": "port", + "version": "Verzia MySensors" + }, + "description": "Nastavenie ethernetovej br\u00e1ny" + }, + "select_gateway_type": { + "description": "Vyberte br\u00e1nu, ktor\u00fa chcete nakonfigurova\u0165.", + "menu_options": { + "gw_mqtt": "Konfigur\u00e1cia br\u00e1ny MQTT", + "gw_serial": "Konfigur\u00e1cia s\u00e9riovej br\u00e1ny", + "gw_tcp": "Konfigur\u00e1cia br\u00e1ny TCP" + } + }, + "user": { + "data": { + "gateway_type": "Typ br\u00e1ny" + }, + "description": "Vyberte sp\u00f4sob pripojenia k br\u00e1ne" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json index 7398b60346a..077987f5dd4 100644 --- a/homeassistant/components/mysensors/translations/sv.json +++ b/homeassistant/components/mysensors/translations/sv.json @@ -34,7 +34,6 @@ "invalid_serial": "Ogiltig serieport", "invalid_subscribe_topic": "Ogiltigt \u00e4mne f\u00f6r prenumeration", "invalid_version": "Ogiltig version av MySensors", - "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad", "not_a_number": "Ange ett nummer", "port_out_of_range": "Portnummer m\u00e5ste vara minst 1 och h\u00f6gst 65535", "same_topic": "\u00c4mnen f\u00f6r prenumeration och publicering \u00e4r desamma", diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index 9fc26a7119b..cd66bb79a0b 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -34,7 +34,6 @@ "invalid_serial": "Ge\u00e7ersiz seri ba\u011flant\u0131 noktas\u0131", "invalid_subscribe_topic": "Ge\u00e7ersiz abone konusu", "invalid_version": "Ge\u00e7ersiz MySensors s\u00fcr\u00fcm\u00fc", - "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", "not_a_number": "L\u00fctfen bir numara giriniz", "port_out_of_range": "Port numaras\u0131 en az 1, en fazla 65535 olmal\u0131d\u0131r", "same_topic": "Abone olma ve yay\u0131nlama konular\u0131 ayn\u0131d\u0131r", diff --git a/homeassistant/components/mysensors/translations/zh-Hans.json b/homeassistant/components/mysensors/translations/zh-Hans.json index a1df0a563a5..bb7390e01e1 100644 --- a/homeassistant/components/mysensors/translations/zh-Hans.json +++ b/homeassistant/components/mysensors/translations/zh-Hans.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "mqtt_required": "\u672a\u914d\u7f6e MQTT \u96c6\u6210" - }, "step": { "gw_mqtt": { "data": { diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index 378624f65b0..a86f7c480ff 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -34,7 +34,6 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", - "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "not_a_number": "\u8acb\u8f38\u5165\u6578\u5b57", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 531dabfde70..0e036a8cdfb 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -81,19 +81,19 @@ def setup_platform( class NAD(MediaPlayerEntity): """Representation of a NAD Receiver.""" + _attr_icon = "mdi:speaker-multiple" _attr_supported_features = SUPPORT_NAD def __init__(self, config): """Initialize the NAD Receiver device.""" self.config = config self._instantiate_nad_receiver() + self._attr_name = self.config[CONF_NAME] self._min_volume = config[CONF_MIN_VOLUME] self._max_volume = config[CONF_MAX_VOLUME] self._source_dict = config[CONF_SOURCE_DICT] self._reverse_mapping = {value: key for key, value in self._source_dict.items()} - self._volume = self._state = self._mute = self._source = None - def _instantiate_nad_receiver(self) -> NADReceiver: if self.config[CONF_TYPE] == "RS232": self._nad_receiver = NADReceiver(self.config[CONF_SERIAL_PORT]) @@ -102,31 +102,6 @@ class NAD(MediaPlayerEntity): port = self.config[CONF_PORT] self._nad_receiver = NADReceiverTelnet(host, port) - @property - def name(self): - """Return the name of the device.""" - return self.config[CONF_NAME] - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def icon(self): - """Return the icon for the device.""" - return "mdi:speaker-multiple" - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._mute - def turn_off(self) -> None: """Turn the media player off.""" self._nad_receiver.main_power("=", "Off") @@ -158,11 +133,6 @@ class NAD(MediaPlayerEntity): """Select input source.""" self._nad_receiver.main_source("=", self._reverse_mapping.get(source)) - @property - def source(self): - """Name of the current input source.""" - return self._source - @property def source_list(self): """List of available input sources.""" @@ -171,27 +141,31 @@ class NAD(MediaPlayerEntity): @property def available(self) -> bool: """Return if device is available.""" - return self._state is not None + return self.state is not None def update(self) -> None: """Retrieve latest state.""" power_state = self._nad_receiver.main_power("?") if not power_state: - self._state = None + self._attr_state = None return - self._state = ( + self._attr_state = ( MediaPlayerState.ON if self._nad_receiver.main_power("?") == "On" else MediaPlayerState.OFF ) - if self._state == MediaPlayerState.ON: - self._mute = self._nad_receiver.main_mute("?") == "On" + if self.state == MediaPlayerState.ON: + self._attr_is_volume_muted = self._nad_receiver.main_mute("?") == "On" volume = self._nad_receiver.main_volume("?") # Some receivers cannot report the volume, e.g. C 356BEE, # instead they only support stepping the volume up or down - self._volume = self.calc_volume(volume) if volume is not None else None - self._source = self._source_dict.get(self._nad_receiver.main_source("?")) + self._attr_volume_level = ( + self.calc_volume(volume) if volume is not None else None + ) + self._attr_source = self._source_dict.get( + self._nad_receiver.main_source("?") + ) def calc_volume(self, decibel): """ @@ -221,38 +195,14 @@ class NADtcp(MediaPlayerEntity): def __init__(self, config): """Initialize the amplifier.""" - self._name = config[CONF_NAME] + self._attr_name = config[CONF_NAME] self._nad_receiver = NADReceiverTCP(config.get(CONF_HOST)) self._min_vol = (config[CONF_MIN_VOLUME] + 90) * 2 # from dB to nad vol (0-200) self._max_vol = (config[CONF_MAX_VOLUME] + 90) * 2 # from dB to nad vol (0-200) self._volume_step = config[CONF_VOLUME_STEP] - self._state = None - self._mute = None self._nad_volume = None - self._volume = None - self._source = None self._source_list = self._nad_receiver.available_sources() - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._mute - def turn_off(self) -> None: """Turn the media player off.""" self._nad_receiver.power_off() @@ -287,11 +237,6 @@ class NADtcp(MediaPlayerEntity): """Select input source.""" self._nad_receiver.select_source(source) - @property - def source(self): - """Name of the current input source.""" - return self._source - @property def source_list(self): """List of available input sources.""" @@ -308,19 +253,19 @@ class NADtcp(MediaPlayerEntity): # Update on/off state if nad_status["power"]: - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF # Update current volume - self._volume = self.nad_vol_to_internal_vol(nad_status["volume"]) + self._attr_volume_level = self.nad_vol_to_internal_vol(nad_status["volume"]) self._nad_volume = nad_status["volume"] # Update muted state - self._mute = nad_status["muted"] + self._attr_is_volume_muted = nad_status["muted"] # Update current source - self._source = nad_status["source"] + self._attr_source = nad_status["source"] def nad_vol_to_internal_vol(self, nad_volume): """Convert nad volume range (0-200) to internal volume range. diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 2b6a74383b5..5e18b94745c 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -22,6 +22,12 @@ ATTR_DHT22_TEMPERATURE: Final = "dht22_temperature" ATTR_HECA_HUMIDITY: Final = "heca_humidity" ATTR_HECA_TEMPERATURE: Final = "heca_temperature" ATTR_MHZ14A_CARBON_DIOXIDE: Final = "mhz14a_carbon_dioxide" +ATTR_PMSX003: Final = "pms" +ATTR_PMSX003_CAQI: Final = f"{ATTR_PMSX003}{SUFFIX_CAQI}" +ATTR_PMSX003_CAQI_LEVEL: Final = f"{ATTR_PMSX003}{SUFFIX_CAQI}_level" +ATTR_PMSX003_P0: Final = f"{ATTR_PMSX003}{SUFFIX_P0}" +ATTR_PMSX003_P1: Final = f"{ATTR_PMSX003}{SUFFIX_P1}" +ATTR_PMSX003_P2: Final = f"{ATTR_PMSX003}{SUFFIX_P2}" ATTR_SDS011: Final = "sds011" ATTR_SDS011_CAQI: Final = f"{ATTR_SDS011}{SUFFIX_CAQI}" ATTR_SDS011_CAQI_LEVEL: Final = f"{ATTR_SDS011}{SUFFIX_CAQI}_level" diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 43c217e2a4d..70da6555a1e 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -3,7 +3,7 @@ "name": "Nettigo Air Monitor", "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], - "requirements": ["nettigo-air-monitor==1.4.2"], + "requirements": ["nettigo-air-monitor==1.5.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 01107baf31b..ce3fdbf16a8 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -43,6 +43,11 @@ from .const import ( ATTR_HECA_HUMIDITY, ATTR_HECA_TEMPERATURE, ATTR_MHZ14A_CARBON_DIOXIDE, + ATTR_PMSX003_CAQI, + ATTR_PMSX003_CAQI_LEVEL, + ATTR_PMSX003_P0, + ATTR_PMSX003_P1, + ATTR_PMSX003_P2, ATTR_SDS011_CAQI, ATTR_SDS011_CAQI_LEVEL, ATTR_SDS011_P1, @@ -136,6 +141,38 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key=ATTR_PMSX003_CAQI, + name="PMSx003 CAQI", + icon="mdi:air-filter", + ), + SensorEntityDescription( + key=ATTR_PMSX003_CAQI_LEVEL, + name="PMSx003 CAQI level", + icon="mdi:air-filter", + device_class="nam__caqi_level", + ), + SensorEntityDescription( + key=ATTR_PMSX003_P0, + name="PMSx003 particulate matter 1.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_PMSX003_P1, + name="PMSx003 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_PMSX003_P2, + name="PMSx003 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key=ATTR_SDS011_CAQI, name="SDS011 CAQI", diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json index 9be1a75603a..69f6ab5783d 100644 --- a/homeassistant/components/nam/translations/bg.json +++ b/homeassistant/components/nam/translations/bg.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "device_unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/nam/translations/sensor.sk.json b/homeassistant/components/nam/translations/sensor.sk.json new file mode 100644 index 00000000000..c0ac0fd36db --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.sk.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Vysok\u00e9", + "low": "N\u00edzke", + "medium": "Stredn\u00e9", + "very high": "Ve\u013emi vysok\u00e9", + "very low": "Ve\u013emi n\u00edzke" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sk.json b/homeassistant/components/nam/translations/sk.json index 71a7aea5018..78966044e1e 100644 --- a/homeassistant/components/nam/translations/sk.json +++ b/homeassistant/components/nam/translations/sk.json @@ -1,10 +1,36 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "device_unsupported": "Zariadenie nie je podporovan\u00e9.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "reauth_unsuccessful": "Op\u00e4tovn\u00e1 autentifik\u00e1cia bola ne\u00faspe\u0161n\u00e1, odstr\u00e1\u0148te integr\u00e1ciu a znova ju nastavte." }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "flow_title": "{host}", + "step": { + "credentials": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte pou\u017e\u00edvate\u013esk\u00e9 meno a heslo." + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte spr\u00e1vne pou\u017e\u00edvate\u013esk\u00e9 meno a heslo pre hostite\u013ea: {host}" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/bg.json b/homeassistant/components/nanoleaf/translations/bg.json index 467fcb0d9bb..36fbff17d7c 100644 --- a/homeassistant/components/nanoleaf/translations/bg.json +++ b/homeassistant/components/nanoleaf/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/nanoleaf/translations/sk.json b/homeassistant/components/nanoleaf/translations/sk.json index c34ad96714a..876c29b2d6d 100644 --- a/homeassistant/components/nanoleaf/translations/sk.json +++ b/homeassistant/components/nanoleaf/translations/sk.json @@ -1,12 +1,31 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } } }, "device_automation": { "trigger_type": { - "swipe_right": "Potiahnite prstom doprava" + "swipe_down": "Potiahnite prstom nadol", + "swipe_left": "Potiahnite prstom do\u013eava", + "swipe_right": "Potiahnite prstom doprava", + "swipe_up": "Potiahnite prstom nahor" } } } \ No newline at end of file diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 2e0e7b56cd6..15a6c454263 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -79,7 +79,7 @@ class NeatoSensor(SensorEntity): return self._robot_serial @property - def device_class(self) -> str: + def device_class(self) -> SensorDeviceClass: """Return the device class.""" return SensorDeviceClass.BATTERY diff --git a/homeassistant/components/neato/translations/bg.json b/homeassistant/components/neato/translations/bg.json index e0e4b9a410d..4a31f71b97e 100644 --- a/homeassistant/components/neato/translations/bg.json +++ b/homeassistant/components/neato/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "create_entry": { "default": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url})." diff --git a/homeassistant/components/neato/translations/sk.json b/homeassistant/components/neato/translations/sk.json index 520a3afd6d9..7baa84a69fb 100644 --- a/homeassistant/components/neato/translations/sk.json +++ b/homeassistant/components/neato/translations/sk.json @@ -1,10 +1,22 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "title": "Chcete za\u010da\u0165 nastavova\u0165?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index f83f914385e..f0b9b4930e1 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -95,9 +95,9 @@ class NestCamera(Camera): return self._device_info.device_model @property - def supported_features(self) -> int: + def supported_features(self) -> CameraEntityFeature: """Flag supported features.""" - supported_features = 0 + supported_features = CameraEntityFeature(0) if CameraLiveStreamTrait.NAME in self._device.traits: supported_features |= CameraEntityFeature.STREAM return supported_features diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index bed44045c11..4a453c5fd38 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -104,7 +104,6 @@ class ThermostatEntity(ClimateEntity): """Initialize ThermostatEntity.""" self._device = device self._device_info = NestDeviceInfo(device) - self._attr_supported_features = 0 @property def unique_id(self) -> str | None: @@ -266,9 +265,9 @@ class ThermostatEntity(ClimateEntity): return FAN_INV_MODES return [] - def _get_supported_features(self) -> int: + def _get_supported_features(self) -> ClimateEntityFeature: """Compute the bitmap of supported features from the current state.""" - features = 0 + features = ClimateEntityFeature(0) if HVACMode.HEAT_COOL in self.hvac_modes: features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE if HVACMode.HEAT in self.hvac_modes or HVACMode.COOL in self.hvac_modes: diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index 13728585e39..de6bd0e3b26 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -97,7 +97,7 @@ class NestThermostat(ClimateEntity): self._fan_modes = [FAN_ON, FAN_AUTO] # Set the default supported features - self._support_flags = ( + self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) @@ -106,7 +106,9 @@ class NestThermostat(ClimateEntity): if self.device.can_heat and self.device.can_cool: self._operation_list.append(HVACMode.AUTO) - self._support_flags |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) # Add supported nest thermostat features if self.device.can_heat: @@ -120,7 +122,7 @@ class NestThermostat(ClimateEntity): # feature of device self._has_fan = self.device.has_fan if self._has_fan: - self._support_flags |= ClimateEntityFeature.FAN_MODE + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE # data attributes self._away = None @@ -150,11 +152,6 @@ class NestThermostat(ClimateEntity): async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) ) - @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - @property def unique_id(self): """Return unique ID for this device.""" diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 90fad5cf185..f0e86456fd7 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.1.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/translations/af.json b/homeassistant/components/nest/translations/af.json deleted file mode 100644 index cedc2123597..00000000000 --- a/homeassistant/components/nest/translations/af.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "auth": { - "data": { - "code": "Hozz\u00e1f\u00e9r\u00e9si token" - }, - "title": "Google fi\u00f3k kapcsol\u00e1sa" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index e3d55a7c84d..a55bd393b6a 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -10,7 +10,6 @@ "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Introdueix un ID de projecte Cloud v\u00e0lid (era el mateix que l'ID de projecte Device Access)" }, "step": { - "auth": { - "data": { - "code": "Token d'acc\u00e9s" - }, - "description": "Per enlla\u00e7ar un compte de Google, [autoritza el compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa a continuaci\u00f3 el codi 'token' d'autenticaci\u00f3 proporcionat.", - "title": "Vinculaci\u00f3 amb compte de Google" - }, "auth_upgrade": { "description": "Google ha deixat d'utilitzar l'autenticaci\u00f3 d'aplicacions per millorar la seguretat i has de crear noves credencials d'aplicaci\u00f3. \n\nConsulta la [documentaci\u00f3]({more_info_url}) i segueix els passos que et guiaran per tornar a tenir acc\u00e9s als teus dispositius Nest.", "title": "Nest: l'autenticaci\u00f3 d'aplicaci\u00f3 s'acaba" diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index b41ce556cfa..32579a47392 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -6,7 +6,6 @@ "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." }, "create_entry": { @@ -19,11 +18,6 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "auth": { - "data": { - "code": "P\u0159\u00edstupov\u00fd token" - } - }, "init": { "data": { "flow_impl": "Poskytovatel" diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index efe0ee8c115..7a9f068443c 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -10,7 +10,6 @@ "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": { @@ -21,18 +20,11 @@ "internal_error": "Ein interner Fehler ist aufgetreten", "invalid_pin": "Ung\u00fcltiger PIN-Code", "subscriber_error": "Unbekannter Abonnentenfehler, siehe Protokolle", - "timeout": "Ein zeit\u00fcberschreitungs Fehler ist aufgetreten", + "timeout": "Ein Zeit\u00fcberschreitungsfehler ist aufgetreten", "unknown": "Unerwarteter Fehler", "wrong_project_id": "Gib eine g\u00fcltige Cloud-Projekt-ID ein (identisch mit der Projekt-ID f\u00fcr den Ger\u00e4tezugriff)." }, "step": { - "auth": { - "data": { - "code": "Zugangstoken" - }, - "description": "Um dein Google-Konto zu verkn\u00fcpfen, w\u00e4hle [Konto autorisieren]({url}).\n\nKopiere nach der Autorisierung den unten angegebenen Authentifizierungstoken-Code.", - "title": "Google-Konto verkn\u00fcpfen" - }, "auth_upgrade": { "description": "App Auth wurde von Google abgeschafft, um die Sicherheit zu verbessern, und du musst Ma\u00dfnahmen ergreifen, indem du neue Anmeldedaten f\u00fcr die Anwendung erstellst.\n\n\u00d6ffnen die [Dokumentation]({more_info_url}) und folge den n\u00e4chsten Schritten, die du durchf\u00fchren musst, um den Zugriff auf deine Nest-Ger\u00e4te wiederherzustellen.", "title": "Nest: Einstellung der App-Authentifizierung" @@ -70,7 +62,7 @@ "data": { "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.", + "description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\nF\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.", "title": "Nest-Konto verkn\u00fcpfen" }, "pick_implementation": { @@ -80,7 +72,7 @@ "data": { "cloud_project_id": "Google Cloud-Projekt-ID" }, - "description": "Rufe die [Cloud Console]( {url} ) auf, um deine Google Cloud-Projekt-ID zu finden.", + "description": "Rufe die [Cloud Console]({url}) auf, um deine Google Cloud-Projekt-ID zu finden.", "title": "Google Cloud konfigurieren" }, "reauth_confirm": { @@ -100,10 +92,10 @@ "issues": { "deprecated_yaml": { "description": "Das Konfigurieren von Nest in configuration.yaml wird in Home Assistant 2022.10 entfernt. \n\nDeine bestehenden OAuth-Anwendungsdaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Nest-YAML-Konfiguration wird entfernt" + "title": "Die Nest YAML-Konfiguration wird entfernt" }, "removed_app_auth": { - "description": "Um die Sicherheit zu verbessern und das Phishing-Risiko zu verringern, hat Google die von Home Assistant verwendete Authentifizierungsmethode eingestellt. \n\n **Zur L\u00f6sung sind Ma\u00dfnahmen deinerseits erforderlich** ([more info]( {more_info_url} )) \n\n 1. Besuche die Integrationsseite\n 1. Dr\u00fccke in der Nest-Integration auf Neu konfigurieren.\n 1. Home Assistant f\u00fchrt dich durch die Schritte zum Upgrade auf die Webauthentifizierung. \n\n Informationen zur Fehlerbehebung findest du in der Nest [Integrationsanleitung]( {documentation_url} ).", + "description": "Um die Sicherheit zu verbessern und das Phishing-Risiko zu verringern, hat Google die von Home Assistant verwendete Authentifizierungsmethode eingestellt. \n\n **Zur L\u00f6sung sind Ma\u00dfnahmen deinerseits erforderlich** ([mehr Information]({more_info_url})) \n\n 1. Besuche die Integrationsseite\n 1. Dr\u00fccke in der Nest-Integration auf Neu konfigurieren.\n 1. Home Assistant f\u00fchrt dich durch die Schritte zum Upgrade auf die Webauthentifizierung. \n\n Informationen zur Fehlerbehebung findest du in der Nest [Integrationsanleitung]({documentation_url}).", "title": "Nest-Authentifizierungsdaten m\u00fcssen aktualisiert werden" } } diff --git a/homeassistant/components/nest/translations/el.json b/homeassistant/components/nest/translations/el.json index 69b6f096d8a..6964337c919 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -10,7 +10,6 @@ "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", "unknown_authorize_url_generation": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 Cloud (\u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2)" }, "step": { - "auth": { - "data": { - "code": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Google, [\u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2]({url}).\n\n\u039c\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7, \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03c4\u03b5-\u03b5\u03c0\u03b9\u03ba\u03bf\u03bb\u03bb\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc Auth Token.", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Google" - }, "auth_upgrade": { "description": "\u03a4\u03bf App Auth \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Google \u03b3\u03b9\u03b1 \u03b2\u03b5\u03bb\u03c4\u03af\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b5\u03c2 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \n\n \u0391\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03bf [documentation]( {more_info_url} ) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03c4\u03b1 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b8\u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03bf\u03c5\u03bd \u03c3\u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Nest.", "title": "Nest: \u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2" diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 07678227547..b29cffc2d91 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,7 +10,6 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "Access Token" - }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "title": "Link Google Account" - }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 95c8aa5ef04..8f919a535f4 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -10,7 +10,6 @@ "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Por favor, introduce un ID de proyecto en la nube v\u00e1lido (era el mismo que el ID del proyecto de acceso al dispositivo)" }, "step": { - "auth": { - "data": { - "code": "Token de acceso" - }, - "description": "Para vincular tu cuenta de Google, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo Auth Token proporcionado a continuaci\u00f3n.", - "title": "Vincular cuenta de Google" - }, "auth_upgrade": { "description": "Google ha dejado de usar App Auth para mejorar la seguridad, y debes tomar medidas creando nuevas credenciales de aplicaci\u00f3n. \n\nAbre la [documentaci\u00f3n]({more_info_url}) para seguir, ya que los siguientes pasos te guiar\u00e1n a trav\u00e9s de los pasos que debes seguir para restaurar el acceso a tus dispositivos Nest.", "title": "Nest: desactivaci\u00f3n de App Auth" diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 655515e93f8..dc882db22da 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -10,7 +10,6 @@ "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", "reauth_successful": "Taastuvastamine \u00f5nnestus", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Sisesta kehtiv pilveprojekti ID (leitud seadme juurdep\u00e4\u00e4su projekti ID)" }, "step": { - "auth": { - "data": { - "code": "Juurdep\u00e4\u00e4sut\u00f5end" - }, - "description": "Oma Google'i konto sidumiseks vali [autoriseeri oma konto]({url}).\n\nP\u00e4rast autoriseerimist kopeeri ja aseta allpool esitatud Auth Token'i kood.", - "title": "Google'i konto linkimine" - }, "auth_upgrade": { "description": "Google on app Authi turvalisuse parandamiseks tauninud ja pead tegutsema, luues uusi rakenduse mandaate.\n\nAva [dokumentatsioon]({more_info_url}), mida j\u00e4rgida, kuna j\u00e4rgmised juhised juhendavad teid nest-seadmetele juurdep\u00e4\u00e4su taastamiseks vajalike juhiste kaudu.", "title": "Nest: App Auth Deprecation" diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 4d7f2d35f06..7ef70e386db 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -7,7 +7,6 @@ "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})", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", - "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": { @@ -23,13 +22,6 @@ "wrong_project_id": "Veuillez saisir un ID de projet Cloud valide (\u00e9tait identique \u00e0 l'ID de projet d'acc\u00e8s \u00e0 l'appareil)" }, "step": { - "auth": { - "data": { - "code": "Jeton d'acc\u00e8s" - }, - "description": "Pour lier votre compte Google, [autorisez votre compte]( {url} ). \n\n Apr\u00e8s autorisation, copiez-collez le code d'authentification fourni ci-dessous.", - "title": "Associer un compte Google" - }, "auth_upgrade": { "title": "Nest\u00a0: abandon de l'authentification d'application" }, diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 7c41202e85e..a3576547944 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -7,7 +7,6 @@ "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", "unknown_authorize_url_generation": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05e9\u05dc \u05d4\u05e8\u05e9\u05d0\u05d4." }, "create_entry": { @@ -20,11 +19,6 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { - "auth": { - "data": { - "code": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" - } - }, "init": { "data": { "flow_impl": "\u05e1\u05e4\u05e7" diff --git a/homeassistant/components/nest/translations/hr.json b/homeassistant/components/nest/translations/hr.json index d12df4db83b..00eb2bf0d16 100644 --- a/homeassistant/components/nest/translations/hr.json +++ b/homeassistant/components/nest/translations/hr.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "authorize_url_timeout": "Isteklo je generiranje URL-a za autorizaciju." + }, + "error": { + "unknown": "Neo\u010dekivana gre\u0161ka" + }, "step": { "init": { "data": { "flow_impl": "Pru\u017eatelj usluge" }, + "description": "Odaberite metodu za autorizaciju", "title": "Pru\u017eatelj usluge autentifikacije" }, "link": { diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 6f868eb7aab..ee372dc0314 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -10,7 +10,6 @@ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "K\u00e9rem, adjon meg egy \u00e9rv\u00e9nyes Cloud Project ID-t (Device Access Project ID-vrl azonos)" }, "step": { - "auth": { - "data": { - "code": "Hozz\u00e1f\u00e9r\u00e9si token" - }, - "description": "[Enged\u00e9lyezze]({url}) Google-fi\u00f3kj\u00e1t az \u00f6sszekapcsol\u00e1hoz.\n\nAz enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja \u00e1t a kapott token k\u00f3dot.", - "title": "\u00d6sszekapcsol\u00e1s Google-al" - }, "auth_upgrade": { "description": "A Google a biztons\u00e1g jav\u00edt\u00e1sa \u00e9rdek\u00e9ben megsz\u00fcntette az App Auth szolg\u00e1ltat\u00e1st, \u00e9s \u00d6nnek \u00faj alkalmaz\u00e1s hiteles\u00edt\u00e9si adatainak l\u00e9trehoz\u00e1s\u00e1val kell tennie valamit. \n\n Nyissa meg a [dokument\u00e1ci\u00f3t]({more_info_url}), hogy k\u00f6vesse, mivel a k\u00f6vetkez\u0151 l\u00e9p\u00e9sek v\u00e9gigvezetik a Nest-eszk\u00f6zeihez val\u00f3 hozz\u00e1f\u00e9r\u00e9s vissza\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges l\u00e9p\u00e9seken.", "title": "Nest: Az alkalmaz\u00e1shiteles\u00edt\u00e9s megsz\u00fcntet\u00e9se" diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 9401016b990..28807e5a46c 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -10,7 +10,6 @@ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", "reauth_successful": "Autentikasi ulang berhasil", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Masukkan ID Proyek Cloud yang valid (sebelumnya sama dengan ID Proyek Akses Perangkat)" }, "step": { - "auth": { - "data": { - "code": "Token Akses" - }, - "description": "Untuk menautkan akun Google Anda, [otorisasi akun Anda]({url}).\n\nSetelah otorisasi, salin dan tempel Token Auth yang disediakan di bawah ini.", - "title": "Tautkan Akun Google" - }, "auth_upgrade": { "description": "Autentikasi Aplikasi tidak digunakan lagi oleh Google untuk meningkatkan keamanan, dan Anda perlu mengambil tindakan dengan membuat kredensial aplikasi baru. \n\nBuka [dokumentasi]({more_info_url}) untuk panduan langkah selanjutnya yang perlu diambil untuk memulihkan akses ke perangkat Nest Anda.", "title": "Nest: Penghentian Autentikasi Aplikasi" diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 6cdfa35b194..dede26423e1 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -10,7 +10,6 @@ "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", - "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." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Inserisci un ID progetto cloud valido (uguale all'ID progetto di accesso al dispositivo)" }, "step": { - "auth": { - "data": { - "code": "Token di accesso" - }, - "description": "Per collegare l'account Google, [authorize your account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito.", - "title": "Connetti l'account Google" - }, "auth_upgrade": { "description": "App Auth \u00e8 stato ritirato da Google per migliorare la sicurezza ed \u00e8 necessario intervenire creando nuove credenziali per l'applicazione. \n\nApri la [documentazione]({more_info_url}) per seguire i passaggi successivi che ti guideranno attraverso gli stadi necessari per ripristinare l'accesso ai tuoi dispositivi Nest.", "title": "Nest: ritiro dell'autenticazione dell'app" diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 68085a7e30a..f93a4ecea9c 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -10,7 +10,6 @@ "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "\u6709\u52b9\u306aCloud Project ID\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044(\u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f)" }, "step": { - "auth": { - "data": { - "code": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" - }, - "description": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f\u3001 [authorize your account]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u8a8d\u8a3c\u5f8c\u3001\u63d0\u4f9b\u3055\u308c\u305f\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u306e\u30b3\u30fc\u30c9\u3092\u4ee5\u4e0b\u306b\u30b3\u30d4\u30fc\u30da\u30fc\u30b9\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" - }, "auth_upgrade": { "description": "App Auth\u306f\u3001\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3092\u5411\u4e0a\u3055\u305b\u308b\u305f\u3081\u306bGoogle\u306b\u3088\u3063\u3066\u5ec3\u6b62\u3055\u308c\u307e\u3057\u305f\u3002\u65b0\u3057\u3044\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u8cc7\u683c\u60c5\u5831\u3092\u4f5c\u6210\u3059\u308b\u3053\u3068\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\n [\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({more_info_url}) \u3092\u958b\u304d\u3001\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Nest\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u5fa9\u5143\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u624b\u9806\u3092\u8aac\u660e\u3057\u307e\u3059\u3002", "title": "\u30cd\u30b9\u30c8: \u30a2\u30d7\u30ea\u8a8d\u8a3c\u306e\u975e\u63a8\u5968" diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json index 1b4563c800f..5e38c522b49 100644 --- a/homeassistant/components/nest/translations/ko.json +++ b/homeassistant/components/nest/translations/ko.json @@ -5,7 +5,6 @@ "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." }, "create_entry": { diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json index 010f34e0dc5..dcf3c42ace5 100644 --- a/homeassistant/components/nest/translations/lb.json +++ b/homeassistant/components/nest/translations/lb.json @@ -4,7 +4,6 @@ "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.", "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": { diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 95ddcd4184d..eee30bf455c 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -7,7 +7,6 @@ "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "reauth_successful": "Herauthenticatie geslaagd", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { @@ -23,13 +22,6 @@ "wrong_project_id": "Voer een geldig Cloud Project ID in (found Device Acces Project ID)" }, "step": { - "auth": { - "data": { - "code": "Toegangstoken" - }, - "description": "Om uw Google account te koppelen, [authoriseer uw account]({url}).\n\nNa autorisatie, copy-paste u de gegeven toegangstoken hieronder.", - "title": "Link Google Account" - }, "init": { "data": { "flow_impl": "Leverancier" diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index c72577c1744..80b842aa622 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -10,7 +10,6 @@ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "reauth_successful": "Re-autentisering var vellykket", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Angi en gyldig Cloud Project ID (var den samme som Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "Tilgangstoken" - }, - "description": "For \u00e5 koble til Google-kontoen din, [autoriser kontoen din]( {url} ). \n\n Etter autorisasjon, kopier og lim inn den oppgitte Auth Token-koden nedenfor.", - "title": "Koble til Google-kontoen" - }, "auth_upgrade": { "description": "App Auth har blitt avviklet av Google for \u00e5 forbedre sikkerheten, og du m\u00e5 iverksette tiltak ved \u00e5 opprette ny applikasjonslegitimasjon. \n\n \u00c5pne [dokumentasjonen]( {more_info_url} ) for \u00e5 f\u00f8lge med, da de neste trinnene vil lede deg gjennom trinnene du m\u00e5 ta for \u00e5 gjenopprette tilgangen til Nest-enhetene dine.", "title": "Nest: Appautentisering avvikelse" diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 11da4335614..48ed71157f4 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -10,7 +10,6 @@ "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" }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Podaj prawid\u0142owy Identyfikator projektu chmury (taki sam jak identyfikator projektu dost\u0119pu do urz\u0105dzenia)" }, "step": { - "auth": { - "data": { - "code": "Token dost\u0119pu" - }, - "description": "Aby po\u0142\u0105czy\u0107 swoje konto Google, [authorize your account]({url}). \n\nPo autoryzacji skopiuj i wklej podany poni\u017cej token uwierzytelniaj\u0105cy.", - "title": "Po\u0142\u0105czenie z kontem Google" - }, "auth_upgrade": { "description": "App Auth zosta\u0142o wycofane przez Google w celu poprawy bezpiecze\u0144stwa i musisz podj\u0105\u0107 dzia\u0142ania, tworz\u0105c nowe dane logowania do aplikacji. \n\nOtw\u00f3rz [dokumentacj\u0119]({more_info_url}), aby przej\u015b\u0107 dalej. Kolejne kroki poprowadz\u0105 Ci\u0119 przez instrukcje, kt\u00f3re musisz wykona\u0107, aby przywr\u00f3ci\u0107 dost\u0119p do urz\u0105dze\u0144 Nest.", "title": "Nest: wycofanie App Auth" diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index 74f68f01775..2fa8a2bab2f 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -10,7 +10,6 @@ "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Insira um ID de projeto da nuvem v\u00e1lido (\u00e9 o mesmo que o ID do projeto de acesso ao dispositivo)" }, "step": { - "auth": { - "data": { - "code": "Token de acesso" - }, - "description": "Para vincular sua conta do Google, [autorize sua conta]( {url} ). \n\n Ap\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo de token de autentica\u00e7\u00e3o fornecido abaixo.", - "title": "Vincular Conta do Google" - }, "auth_upgrade": { "description": "A autentica\u00e7\u00e3o de aplicativo foi preterida pelo Google para melhorar a seguran\u00e7a, e voc\u00ea precisa agir criando novas credenciais de aplicativo. \n\n Abra a [documenta\u00e7\u00e3o]( {more_info_url} ) para acompanhar, pois as pr\u00f3ximas etapas o guiar\u00e3o pelas etapas necess\u00e1rias para restaurar o acesso aos seus dispositivos Nest.", "title": "Nest: suspens\u00e3o de uso da autentica\u00e7\u00e3o do aplicativo" diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 4e5c4c2c34a..e1022b86b01 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -6,7 +6,6 @@ "invalid_access_token": "Token de acesso inv\u00e1lido", "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.", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { @@ -19,11 +18,6 @@ "unknown": "Erro inesperado" }, "step": { - "auth": { - "data": { - "code": "Token de Acesso" - } - }, "init": { "data": { "flow_impl": "Fornecedor" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 5a995fb35ae..df461ad87a4 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -10,7 +10,6 @@ "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 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "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.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \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." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u0442\u0430\u043a\u043e\u0439 \u0436\u0435 \u043a\u0430\u043a \u0438 Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" - }, - "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google. \n\n\u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", - "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Google" - }, "auth_upgrade": { "description": "\u041f\u0440\u0435\u0436\u043d\u0438\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0431\u044b\u043b \u0443\u043f\u0440\u0430\u0437\u0434\u043d\u0435\u043d Google \u0434\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438. \u0412\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\n\n\u0427\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c Nest \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]({more_info_url}).", "title": "Nest: \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" diff --git a/homeassistant/components/nest/translations/sk.json b/homeassistant/components/nest/translations/sk.json index 43b57c320d4..c8926d87222 100644 --- a/homeassistant/components/nest/translations/sk.json +++ b/homeassistant/components/nest/translations/sk.json @@ -1,20 +1,66 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown_authorize_url_generation": "Nezn\u00e1ma chyba pri generovan\u00ed autorizovanej adresy URL." }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" }, "error": { - "invalid_pin": "Nespr\u00e1vny PIN" + "internal_error": "Intern\u00e1 chyba pri overovan\u00ed k\u00f3du", + "invalid_pin": "Nespr\u00e1vny PIN", + "subscriber_error": "Nezn\u00e1ma chyba odberate\u013ea, pozrite si denn\u00edky", + "timeout": "Overovac\u00ed k\u00f3d \u010dasov\u00e9ho limitu", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { - "auth": { + "cloud_project": { "data": { - "code": "Pr\u00edstupov\u00fd token" - } + "cloud_project_id": "ID projektu Google Cloud" + }, + "title": "Hniezdo: Zadajte ID projektu Cloud" + }, + "init": { + "description": "Vyberte met\u00f3du overenia" + }, + "link": { + "data": { + "code": "PIN k\u00f3d" + }, + "description": "Ak chcete prepoji\u0165 svoje konto Nest, [autorizujte svoje konto]({url}).\n\nPo autoriz\u00e1cii skop\u00edrujte a vlo\u017ete poskytnut\u00fd k\u00f3d PIN uveden\u00fd ni\u017e\u0161ie." + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "pubsub": { + "data": { + "cloud_project_id": "ID projektu Google Cloud" + }, + "description": "Nav\u0161t\u00edvte [Cloud Console]({url}) a vyh\u013eadajte svoje ID projektu Google Cloud.", + "title": "Nakonfigurujte Google Cloud" + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Zisten\u00fd pohyb", + "camera_person": "Zisten\u00e1 osoba", + "camera_sound": "Rozpoznan\u00fd zvuk", + "doorbell_chime": "Zvon\u010dek stla\u010den\u00fd" + } + }, + "issues": { + "deprecated_yaml": { + "title": "Konfigur\u00e1cia Nest YAML sa odstra\u0148uje" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/sl.json b/homeassistant/components/nest/translations/sl.json index 836ae7761e8..84f07fdcf42 100644 --- a/homeassistant/components/nest/translations/sl.json +++ b/homeassistant/components/nest/translations/sl.json @@ -11,12 +11,6 @@ "unknown": "Neznana napaka pri preverjanju kode" }, "step": { - "auth": { - "data": { - "code": "\u017deton za dostop" - }, - "title": "Pove\u017eite Google Ra\u010dun" - }, "init": { "data": { "flow_impl": "Ponudnik" diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 3eae7e1c547..42ea1a7a70f 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -10,7 +10,6 @@ "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", "reauth_successful": "\u00c5terautentisering lyckades", - "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Ange ett giltigt Cloud Project ID (var samma som Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "\u00c5tkomstnyckel" - }, - "description": "F\u00f6r att l\u00e4nka ditt Google-konto, [auktorisera ditt konto]( {url} ). \n\n Efter auktorisering, kopiera och klistra in den medf\u00f6ljande Auth Token-koden nedan.", - "title": "L\u00e4nka Google-konto" - }, "auth_upgrade": { "description": "App Auth har fasats ut av Google f\u00f6r att f\u00f6rb\u00e4ttra s\u00e4kerheten, och du m\u00e5ste vidta \u00e5tg\u00e4rder genom att skapa nya applikationsuppgifter. \n\n \u00d6ppna [dokumentationen]( {more_info_url} ) f\u00f6r att f\u00f6lja med eftersom n\u00e4sta steg guidar dig genom stegen du beh\u00f6ver ta f\u00f6r att \u00e5terst\u00e4lla \u00e5tkomsten till dina Nest-enheter.", "title": "Nest: Utfasning av appautentisering" diff --git a/homeassistant/components/nest/translations/th.json b/homeassistant/components/nest/translations/th.json index 99efbb30cad..5f14558e2b5 100644 --- a/homeassistant/components/nest/translations/th.json +++ b/homeassistant/components/nest/translations/th.json @@ -1,9 +1,6 @@ { "config": { "step": { - "auth": { - "title": "\u0e40\u0e0a\u0e37\u0e48\u0e2d\u0e21\u0e15\u0e48\u0e2d\u0e1a\u0e31\u0e0d\u0e0a\u0e35\u0e02\u0e2d\u0e07 oogle" - }, "link": { "data": { "code": "Pin code" diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 4e52843ca51..80d72d93d2a 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -10,7 +10,6 @@ "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", "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." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (Cihaz Eri\u015fimi Proje Kimli\u011fi ile ayn\u0131yd\u0131)" }, "step": { - "auth": { - "data": { - "code": "Eri\u015fim Anahtar\u0131" - }, - "description": "Google hesab\u0131n\u0131z\u0131 ba\u011flamak i\u00e7in [hesab\u0131n\u0131z\u0131 yetkilendirin]( {url} ). \n\n Yetkilendirmeden sonra, sa\u011flanan Auth Token kodunu a\u015fa\u011f\u0131ya kopyalay\u0131p yap\u0131\u015ft\u0131r\u0131n.", - "title": "Google Hesab\u0131n\u0131 Ba\u011fla" - }, "auth_upgrade": { "description": "App Auth, g\u00fcenli\u011fi art\u0131rmak i\u00e7in Google taraf\u0131ndan kullan\u0131mdan kald\u0131r\u0131ld\u0131 ve yeni uygulama kimlik bilgileri olu\u015fturarak i\u015flem yapman\u0131z gerekiyor. \n\n Takip etmek i\u00e7in [belgeleri]( {more_info_url} ) a\u00e7\u0131n, \u00e7\u00fcnk\u00fc sonraki ad\u0131mlar Nest cihazlar\u0131n\u0131za eri\u015fimi geri y\u00fcklemek i\u00e7in atman\u0131z gereken ad\u0131mlar konusunda size rehberlik edecektir.", "title": "Nest: Uygulama Yetkilendirmesinin Kullan\u0131mdan Kald\u0131r\u0131lmas\u0131" diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json index cfdb2c91ee2..7f09a23e493 100644 --- a/homeassistant/components/nest/translations/uk.json +++ b/homeassistant/components/nest/translations/uk.json @@ -6,7 +6,6 @@ "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": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f.", "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": { diff --git a/homeassistant/components/nest/translations/zh-Hans.json b/homeassistant/components/nest/translations/zh-Hans.json index c033f8be626..3d481dec9d5 100644 --- a/homeassistant/components/nest/translations/zh-Hans.json +++ b/homeassistant/components/nest/translations/zh-Hans.json @@ -10,10 +10,6 @@ "unknown": "\u9a8c\u8bc1\u7801\u672a\u77e5\u9519\u8bef" }, "step": { - "auth": { - "description": "\u8981\u5173\u8054\u60a8\u7684 Google \u5e10\u6237\uff0c\u8bf7[\u524d\u5f80\u6388\u6743]({url})\u3002 \n\n\u6388\u6743\u6210\u529f\u540e\uff0c\u8bf7\u590d\u5236\u4e0b\u65b9\u7684 Auth Token \u4ee3\u7801\u3002", - "title": "\u5173\u8054 Google \u5e10\u6237" - }, "init": { "data": { "flow_impl": "\u8ba4\u8bc1\u63d0\u4f9b\u8005" diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index a0ff9cab7f8..7780b42cb84 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -10,7 +10,6 @@ "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "\u8acb\u8f38\u5165\u6709\u6548 Cloud \u5c08\u6848 ID\uff08\u8207\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ID \u76f8\u540c\uff09" }, "step": { - "auth": { - "data": { - "code": "\u5b58\u53d6\u6b0a\u6756" - }, - "description": "\u6b32\u9023\u7d50 Google \u5e33\u865f\u3001\u8acb\u5148 [\u8a8d\u8b49\u5e33\u865f]({url})\u3002\n\n\u65bc\u8a8d\u8b49\u5f8c\u3001\u65bc\u4e0b\u65b9\u8cbc\u4e0a\u8a8d\u8b49\u6b0a\u6756\u4ee3\u78bc\u3002", - "title": "\u9023\u7d50 Google \u5e33\u865f" - }, "auth_upgrade": { "description": "Google \u5df2\u4e0d\u518d\u63a8\u85a6\u4f7f\u7528 App Auth \u4ee5\u63d0\u9ad8\u5b89\u5168\u6027\u3001\u56e0\u6b64\u60a8\u9700\u8981\u5efa\u7acb\u65b0\u7684\u61c9\u7528\u7a0b\u5f0f\u6191\u8b49\u3002\n\n\u958b\u555f [\u76f8\u95dc\u6587\u4ef6]({more_info_url}) \u4e26\u8ddf\u96a8\u6b65\u9a5f\u6307\u5f15\u3001\u5c07\u5e36\u9818\u60a8\u5b58\u53d6\u6216\u56de\u5fa9\u60a8\u7684 Nest \u88dd\u7f6e\u3002", "title": "Nest: App Auth \u5df2\u4e0d\u63a8\u85a6\u4f7f\u7528" diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 9254ff6e284..01c459acaea 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -179,9 +179,9 @@ class NetatmoCamera(NetatmoBase, Camera): return None @property - def supported_features(self) -> int: + def supported_features(self) -> CameraEntityFeature: """Return supported features.""" - supported_features: int = CameraEntityFeature.ON_OFF + supported_features = CameraEntityFeature.ON_OFF if self._model != "NDB": supported_features |= CameraEntityFeature.STREAM return supported_features diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index e93d0c91a07..21f821040dd 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -21,6 +21,7 @@ CONF_URL_SECURITY = "https://home.netatmo.com/security" CONF_URL_ENERGY = "https://my.netatmo.com/app/energy" CONF_URL_WEATHER = "https://my.netatmo.com/app/weather" CONF_URL_CONTROL = "https://home.netatmo.com/control" +CONF_URL_PUBLIC_WEATHER = "https://weathermap.netatmo.com/" AUTH = "netatmo_auth" CONF_PUBLIC = "public_sensor_config" diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 0d4681197bd..ac478282614 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -1,6 +1,7 @@ { "domain": "netatmo", "name": "Netatmo", + "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": ["pyatmo==7.4.0"], "after_dependencies": ["cloud", "media_source"], diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 82f6c95b699..1fed7cf5e0e 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -39,6 +39,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_URL_ENERGY, + CONF_URL_PUBLIC_WEATHER, CONF_URL_WEATHER, CONF_WEATHER_AREAS, DATA_HANDLER, @@ -702,6 +703,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._device_name = f"{self._area_name}" self._attr_name = f"{description.name}" self._show_on_map = area.show_on_map + self._config_url = CONF_URL_PUBLIC_WEATHER self._attr_unique_id = ( f"{self._device_name.replace(' ', '-')}-{description.key}" ) diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index 5e771b9224b..d458bae9e8e 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index b2210a4375c..4511296efa5 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -24,7 +24,7 @@ "trigger_subtype": { "away": "Fuori casa", "hg": "protezione antigelo", - "schedule": "programma" + "schedule": "calendarizzazione" }, "trigger_type": { "alarm_started": "{entity_name} ha rilevato un allarme", diff --git a/homeassistant/components/netatmo/translations/sk.json b/homeassistant/components/netatmo/translations/sk.json index 0f73e9340d9..80589b6b4a6 100644 --- a/homeassistant/components/netatmo/translations/sk.json +++ b/homeassistant/components/netatmo/translations/sk.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" @@ -9,18 +13,49 @@ "step": { "pick_implementation": { "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" } } }, + "device_automation": { + "trigger_type": { + "alarm_started": "{entity_name} rozpoznala alarm", + "animal": "{entity_name} rozpoznala zviera", + "human": "{entity_name} rozpoznala \u010dloveka", + "movement": "{entity_name} rozpoznala pohyb", + "outdoor": "{n\u00e1zov_objektu} zistila vonkaj\u0161iu udalos\u0165", + "person": "{entity_name} rozpoznala osobu", + "person_away": "{entity_name} zistila, \u017ee osoba odi\u0161la", + "set_point": "Cie\u013eov\u00e1 teplota {entity_name} nastaven\u00e1 manu\u00e1lne", + "therm_mode": "{entity_name} prepnut\u00e9 na \u201e{subtype}\u201c", + "turned_off": "{entity_name} vypnut\u00e1", + "turned_on": "{entity_name} zapnut\u00e1" + } + }, "options": { "step": { "public_weather": { "data": { + "area_name": "N\u00e1zov oblasti", "lat_ne": "Zemepisn\u00e1 \u0161\u00edrka: severov\u00fdchodn\u00fd roh", "lat_sw": "Zemepisn\u00e1 \u0161\u00edrka: juhoz\u00e1padn\u00fd roh", "lon_ne": "Zemepisn\u00e1 d\u013a\u017eka: severov\u00fdchodn\u00fd roh", - "lon_sw": "Zemepisn\u00e1 d\u013a\u017eka: juhoz\u00e1padn\u00fd roh" - } + "lon_sw": "Zemepisn\u00e1 d\u013a\u017eka: juhoz\u00e1padn\u00fd roh", + "mode": "V\u00fdpo\u010det", + "show_on_map": "Zobrazi\u0165 na mape" + }, + "description": "Nakonfigurujte verejn\u00fd senzor po\u010dasia pre dan\u00fa oblas\u0165.", + "title": "Verejn\u00fd senzor po\u010dasia Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "N\u00e1zov oblasti", + "weather_areas": "Poveternostn\u00e9 oblasti" + }, + "description": "Nakonfigurujte verejn\u00e9 senzory po\u010dasia.", + "title": "Verejn\u00fd senzor po\u010dasia Netatmo" } } } diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 5feff521efa..ffb33d5ebeb 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -3,8 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/netgear/translations/cs.json b/homeassistant/components/netgear/translations/cs.json index 67e2611aa87..e96fd714d69 100644 --- a/homeassistant/components/netgear/translations/cs.json +++ b/homeassistant/components/netgear/translations/cs.json @@ -9,7 +9,8 @@ "host": "Hostitel (nepovinn\u00fd)", "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no (nepovinn\u00e9)" - } + }, + "description": "V\u00fdchoz\u00ed hostitel: {host}\nV\u00fdchoz\u00ed u\u017eivatelsk\u00e9 jm\u00e9no: {username}" } } } diff --git a/homeassistant/components/netgear/translations/sk.json b/homeassistant/components/netgear/translations/sk.json new file mode 100644 index 00000000000..ae8937fb96d --- /dev/null +++ b/homeassistant/components/netgear/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "config": "Chyba pripojenia alebo prihl\u00e1senia: skontrolujte konfigur\u00e1ciu" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e (Volite\u013en\u00e9)", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno (volite\u013en\u00e9)" + }, + "description": "Predvolen\u00fd hostite\u013e: {host}\nPredvolen\u00e9 pou\u017e\u00edvate\u013esk\u00e9 meno: {username}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index f8c08b4efd6..aa7c55df33f 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -166,12 +166,11 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): self._has_emergency_heat = self._thermostat.has_emergency_heat() self._has_humidify_support = self._thermostat.has_humidify_support() self._has_dehumidify_support = self._thermostat.has_dehumidify_support() - supported = NEXIA_SUPPORTED + self._attr_supported_features = NEXIA_SUPPORTED if self._has_humidify_support or self._has_dehumidify_support: - supported |= ClimateEntityFeature.TARGET_HUMIDITY + self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY if self._has_emergency_heat: - supported |= ClimateEntityFeature.AUX_HEAT - self._attr_supported_features = supported + self._attr_supported_features |= ClimateEntityFeature.AUX_HEAT self._attr_preset_modes = self._zone.get_presets() self._attr_fan_modes = self._thermostat.get_fan_modes() self._attr_hvac_modes = HVAC_MODES diff --git a/homeassistant/components/nexia/translations/bg.json b/homeassistant/components/nexia/translations/bg.json index 7aa8fb275ea..5ef9f8721aa 100644 --- a/homeassistant/components/nexia/translations/bg.json +++ b/homeassistant/components/nexia/translations/bg.json @@ -4,6 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/nexia/translations/sk.json b/homeassistant/components/nexia/translations/sk.json index 5ada995aa6e..0c9a112e32e 100644 --- a/homeassistant/components/nexia/translations/sk.json +++ b/homeassistant/components/nexia/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 5ab5d79caf7..4f24a7aa7f3 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -122,7 +122,7 @@ class NextBusDepartureSensor(SensorEntity): both the route and the stop. This is possibly a little convoluted to provide as it requires making a - request to the service to get these values. Perhaps it can be simplifed in + request to the service to get these values. Perhaps it can be simplified in the future using fuzzy logic and matching. """ diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 2a68107079e..5538a759ff2 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.1.1"], + "requirements": ["nextdns==1.2.2"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"], diff --git a/homeassistant/components/nextdns/translations/de.json b/homeassistant/components/nextdns/translations/de.json index 51a5eaf5edd..e8a50c3dba9 100644 --- a/homeassistant/components/nextdns/translations/de.json +++ b/homeassistant/components/nextdns/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieses NextDNS-Profil ist bereits konfiguriert." + "already_configured": "Dieses NextDNS Profil ist bereits konfiguriert." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/nextdns/translations/nl.json b/homeassistant/components/nextdns/translations/nl.json index 436e1a68b7d..73a61b8a34d 100644 --- a/homeassistant/components/nextdns/translations/nl.json +++ b/homeassistant/components/nextdns/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dit NextDNS profiel is al geconfigureerd." + }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_api_key": "Ongeldige API-sleutel", @@ -17,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Server bereikbaar" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/sk.json b/homeassistant/components/nextdns/translations/sk.json new file mode 100644 index 00000000000..a748bf158fb --- /dev/null +++ b/homeassistant/components/nextdns/translations/sk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Tento profil NextDNS je u\u017e nakonfigurovan\u00fd." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index df285bea228..fc05c2c12a1 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_push", - "loggers": ["notifications_android_tv"] + "loggers": ["notifications_android_tv"], + "integration_type": "service" } diff --git a/homeassistant/components/nfandroidtv/translations/de.json b/homeassistant/components/nfandroidtv/translations/de.json index fef64459261..63161696967 100644 --- a/homeassistant/components/nfandroidtv/translations/de.json +++ b/homeassistant/components/nfandroidtv/translations/de.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Name" }, - "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind." + "description": "Bitte lese die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind." } } } diff --git a/homeassistant/components/nfandroidtv/translations/sk.json b/homeassistant/components/nfandroidtv/translations/sk.json index af15f92c2f2..fc84825ae2e 100644 --- a/homeassistant/components/nfandroidtv/translations/sk.json +++ b/homeassistant/components/nfandroidtv/translations/sk.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov" - } + }, + "description": "Pozrite si dokument\u00e1ciu, aby ste sa uistili, \u017ee s\u00fa splnen\u00e9 v\u0161etky po\u017eiadavky." } } } diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 053d6db2a34..68e16871549 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -10,10 +10,10 @@ from typing import Any, Generic, TypeVar from nibe.coil import Coil from nibe.connection import Connection +from nibe.connection.modbus import Modbus from nibe.connection.nibegw import NibeGW from nibe.exceptions import CoilNotFoundException, CoilReadException -from nibe.heatpump import HeatPump, Model -from tenacity import RetryError, retry, retry_if_exception_type, stop_after_attempt +from nibe.heatpump import HeatPump, Model, Series from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -34,8 +34,11 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_MODBUS, CONF_CONNECTION_TYPE_NIBEGW, CONF_LISTENING_PORT, + CONF_MODBUS_UNIT, + CONF_MODBUS_URL, CONF_REMOTE_READ_PORT, CONF_REMOTE_WRITE_PORT, CONF_WORD_SWAP, @@ -57,12 +60,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nibe Heat Pump from a config entry.""" heatpump = HeatPump(Model[entry.data[CONF_MODEL]]) - heatpump.word_swap = entry.data[CONF_WORD_SWAP] - await hass.async_add_executor_job(heatpump.initialize) + await heatpump.initialize() + connection: Connection connection_type = entry.data[CONF_CONNECTION_TYPE] if connection_type == CONF_CONNECTION_TYPE_NIBEGW: + heatpump.word_swap = entry.data[CONF_WORD_SWAP] connection = NibeGW( heatpump, entry.data[CONF_IP_ADDRESS], @@ -70,13 +74,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_REMOTE_WRITE_PORT], listening_port=entry.data[CONF_LISTENING_PORT], ) + elif connection_type == CONF_CONNECTION_TYPE_MODBUS: + connection = Modbus( + heatpump, entry.data[CONF_MODBUS_URL], entry.data[CONF_MODBUS_UNIT] + ) else: raise HomeAssistantError(f"Connection type {connection_type} is not supported.") await connection.start() + assert heatpump.model + + async def _async_stop(_): + await connection.stop() + entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, connection.stop) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) ) coordinator = Coordinator(hass, heatpump, connection) @@ -184,6 +197,11 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]): self.seed[coil.address] = coil self.async_update_context_listeners([coil.address]) + @property + def series(self) -> Series: + """Return which series of pump we are connected to.""" + return self.heatpump.series + @property def coils(self) -> list[Coil]: """Return the full coil database.""" @@ -201,8 +219,8 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]): def get_coil_value(self, coil: Coil) -> int | str | float | None: """Return a coil with data and check for validity.""" - if coil := self.data.get(coil.address): - return coil.value + if coil_with_data := self.data.get(coil.address): + return coil_with_data.value return None def get_coil_float(self, coil: Coil) -> float | None: @@ -228,33 +246,29 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]): self.task = None async def _async_update_data_internal(self) -> dict[int, Coil]: - @retry( - retry=retry_if_exception_type(CoilReadException), - stop=stop_after_attempt(COIL_READ_RETRIES), - ) - async def read_coil(coil: Coil): - return await self.connection.read_coil(coil) result: dict[int, Coil] = {} - for address in self.context_callbacks.keys(): - if seed := self.seed.pop(address, None): - self.logger.debug("Skipping seeded coil: %d", address) - result[address] = seed - continue + def _get_coils() -> Iterable[Coil]: + for address in sorted(self.context_callbacks.keys()): + if seed := self.seed.pop(address, None): + self.logger.debug("Skipping seeded coil: %d", address) + result[address] = seed + continue - try: - coil = self.heatpump.get_coil_by_address(address) - except CoilNotFoundException as exception: - self.logger.debug("Skipping missing coil: %s", exception) - continue + try: + coil = self.heatpump.get_coil_by_address(address) + except CoilNotFoundException as exception: + self.logger.debug("Skipping missing coil: %s", exception) + continue + yield coil - try: - result[coil.address] = await read_coil(coil) - except (CoilReadException, RetryError) as exception: - raise UpdateFailed(f"Failed to update: {exception}") from exception - - self.seed.pop(coil.address, None) + try: + async for coil in self.connection.read_coils(_get_coils()): + result[coil.address] = coil + self.seed.pop(coil.address, None) + except CoilReadException as exception: + raise UpdateFailed(f"Failed to update: {exception}") from exception return result diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index d68def046fd..6050010b20d 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -1,14 +1,21 @@ """Config flow for Nibe Heat Pump integration.""" from __future__ import annotations -import errno -from socket import gaierror from typing import Any +from nibe.connection.modbus import Modbus from nibe.connection.nibegw import NibeGW -from nibe.exceptions import CoilNotFoundException, CoilReadException, CoilWriteException +from nibe.exceptions import ( + AddressInUseException, + CoilNotFoundException, + CoilReadException, + CoilReadSendException, + CoilWriteException, + CoilWriteSendException, +) from nibe.heatpump import HeatPump, Model import voluptuous as vol +import yarl from homeassistant import config_entries from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL @@ -18,8 +25,11 @@ from homeassistant.helpers import selector from .const import ( CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_MODBUS, CONF_CONNECTION_TYPE_NIBEGW, CONF_LISTENING_PORT, + CONF_MODBUS_UNIT, + CONF_MODBUS_URL, CONF_REMOTE_READ_PORT, CONF_REMOTE_WRITE_PORT, CONF_WORD_SWAP, @@ -36,7 +46,7 @@ PORT_SELECTOR = vol.All( vol.Coerce(int), ) -STEP_USER_DATA_SCHEMA = vol.Schema( +STEP_NIBEGW_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_MODEL): vol.In(list(Model.__members__)), vol.Required(CONF_IP_ADDRESS): selector.TextSelector(), @@ -47,6 +57,22 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) +STEP_MODBUS_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_MODEL): vol.In(list(Model.__members__)), + vol.Required(CONF_MODBUS_URL): selector.TextSelector(), + vol.Required(CONF_MODBUS_UNIT, default=0): vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, step=1, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Coerce(int), + ), + } +) + + class FieldError(Exception): """Field with invalid data.""" @@ -57,11 +83,13 @@ class FieldError(Exception): self.error = error -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: +async def validate_nibegw_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, dict[str, Any]]: """Validate the user input allows us to connect.""" heatpump = HeatPump(Model[data[CONF_MODEL]]) - heatpump.initialize() + await heatpump.initialize() connection = NibeGW( heatpump, @@ -73,24 +101,17 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, try: await connection.start() - except OSError as exception: - if exception.errno == errno.EADDRINUSE: - raise FieldError( - "Address already in use", "listening_port", "address_in_use" - ) from exception - raise + except AddressInUseException as exception: + raise FieldError( + "Address already in use", "listening_port", "address_in_use" + ) from exception try: - coil = heatpump.get_coil_by_name("modbus40-word-swap-48852") - coil = await connection.read_coil(coil) - word_swap = coil.value == "ON" - coil = await connection.write_coil(coil) - except gaierror as exception: - raise FieldError(str(exception), "ip_address", "address") from exception + await connection.verify_connectivity() + except (CoilReadSendException, CoilWriteSendException) as exception: + raise FieldError(str(exception), CONF_IP_ADDRESS, "address") from exception except CoilNotFoundException as exception: - raise FieldError( - "Model selected doesn't seem to support expected coils", "base", "model" - ) from exception + raise FieldError("Coils not found", "base", "model") from exception except CoilReadException as exception: raise FieldError("Timeout on read from pump", "base", "read") from exception except CoilWriteException as exception: @@ -98,9 +119,49 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, finally: await connection.stop() - return { - "title": f"{data[CONF_MODEL]} at {data[CONF_IP_ADDRESS]}", - CONF_WORD_SWAP: word_swap, + return f"{data[CONF_MODEL]} at {data[CONF_IP_ADDRESS]}", { + **data, + CONF_WORD_SWAP: heatpump.word_swap, + CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_NIBEGW, + } + + +async def validate_modbus_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, dict[str, Any]]: + """Validate the user input allows us to connect.""" + + heatpump = HeatPump(Model[data[CONF_MODEL]]) + await heatpump.initialize() + + try: + connection = Modbus( + heatpump, + data[CONF_MODBUS_URL], + data[CONF_MODBUS_UNIT], + ) + except ValueError as exc: + raise FieldError("Not a valid modbus url", CONF_MODBUS_URL, "url") from exc + + await connection.start() + + try: + await connection.verify_connectivity() + except (CoilReadSendException, CoilWriteSendException) as exception: + raise FieldError(str(exception), CONF_MODBUS_URL, "address") from exception + except CoilNotFoundException as exception: + raise FieldError("Coils not found", "base", "model") from exception + except CoilReadException as exception: + raise FieldError("Timeout on read from pump", "base", "read") from exception + except CoilWriteException as exception: + raise FieldError("Timeout on writing to pump", "base", "write") from exception + finally: + await connection.stop() + + host = yarl.URL(data[CONF_MODBUS_URL]).host + return f"{data[CONF_MODEL]} at {host}", { + **data, + CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_MODBUS, } @@ -113,15 +174,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" + return self.async_show_menu(step_id="user", menu_options=["modbus", "nibegw"]) + + async def async_step_modbus( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the modbus step.""" if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="modbus", data_schema=STEP_MODBUS_DATA_SCHEMA ) errors = {} try: - info = await validate_input(self.hass, user_input) + title, data = await validate_modbus_input(self.hass, user_input) except FieldError as exception: LOGGER.debug("Validation error %s", exception) errors[exception.field] = exception.error @@ -129,13 +196,34 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - data = { - **user_input, - CONF_WORD_SWAP: info[CONF_WORD_SWAP], - CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_NIBEGW, - } - return self.async_create_entry(title=info["title"], data=data) + return self.async_create_entry(title=title, data=data) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="modbus", data_schema=STEP_MODBUS_DATA_SCHEMA, errors=errors + ) + + async def async_step_nibegw( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the nibegw step.""" + if user_input is None: + return self.async_show_form( + step_id="nibegw", data_schema=STEP_NIBEGW_DATA_SCHEMA + ) + + errors = {} + + try: + title, data = await validate_nibegw_input(self.hass, user_input) + except FieldError as exception: + LOGGER.exception("Validation error") + errors[exception.field] = exception.error + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=title, data=data) + + return self.async_show_form( + step_id="nibegw", data_schema=STEP_NIBEGW_DATA_SCHEMA, errors=errors ) diff --git a/homeassistant/components/nibe_heatpump/const.py b/homeassistant/components/nibe_heatpump/const.py index f1bcbf11127..381ad7ba0c2 100644 --- a/homeassistant/components/nibe_heatpump/const.py +++ b/homeassistant/components/nibe_heatpump/const.py @@ -10,3 +10,6 @@ CONF_REMOTE_WRITE_PORT = "remote_write_port" CONF_WORD_SWAP = "word_swap" CONF_CONNECTION_TYPE = "connection_type" CONF_CONNECTION_TYPE_NIBEGW = "nibegw" +CONF_CONNECTION_TYPE_MODBUS = "modbus" +CONF_MODBUS_URL = "modbus_url" +CONF_MODBUS_UNIT = "modbus_unit" diff --git a/homeassistant/components/nibe_heatpump/manifest.json b/homeassistant/components/nibe_heatpump/manifest.json index 4b66b93d31b..f9276570885 100644 --- a/homeassistant/components/nibe_heatpump/manifest.json +++ b/homeassistant/components/nibe_heatpump/manifest.json @@ -3,7 +3,7 @@ "name": "Nibe Heat Pump", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nibe_heatpump", - "requirements": ["nibe==0.5.0", "tenacity==8.0.1"], + "requirements": ["nibe==1.3.0"], "codeowners": ["@elupus"], "iot_class": "local_polling" } diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py index 11c6917ec1c..606588f7142 100644 --- a/homeassistant/components/nibe_heatpump/number.py +++ b/homeassistant/components/nibe_heatpump/number.py @@ -58,6 +58,10 @@ class Number(CoilEntity, NumberEntity): self._attr_native_value = None def _async_read_coil(self, coil: Coil) -> None: + if coil.value is None: + self._attr_native_value = None + return + try: self._attr_native_value = float(coil.value) except ValueError: diff --git a/homeassistant/components/nibe_heatpump/select.py b/homeassistant/components/nibe_heatpump/select.py index 27df1980287..412c1579586 100644 --- a/homeassistant/components/nibe_heatpump/select.py +++ b/homeassistant/components/nibe_heatpump/select.py @@ -35,11 +35,16 @@ class Select(CoilEntity, SelectEntity): def __init__(self, coordinator: Coordinator, coil: Coil) -> None: """Initialize entity.""" + assert coil.mappings super().__init__(coordinator, coil, ENTITY_ID_FORMAT) self._attr_options = list(coil.mappings.values()) self._attr_current_option = None def _async_read_coil(self, coil: Coil) -> None: + if not isinstance(coil.value, str): + self._attr_current_option = None + return + self._attr_current_option = coil.value async def async_select_option(self, option: str) -> None: diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index 0b12afd9e05..c092464b1bf 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -16,14 +16,10 @@ from homeassistant.const import ( ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - ENERGY_MEGA_WATT_HOUR, - ENERGY_WATT_HOUR, - POWER_KILO_WATT, - POWER_WATT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, TIME_HOURS, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory @@ -37,14 +33,14 @@ UNIT_DESCRIPTIONS = { entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), "°F": SensorEntityDescription( key="°F", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_FAHRENHEIT, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, ), "A": SensorEntityDescription( key="A", @@ -79,35 +75,35 @@ UNIT_DESCRIPTIONS = { entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, ), "kW": SensorEntityDescription( key="kW", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, ), "Wh": SensorEntityDescription( key="Wh", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, ), "kWh": SensorEntityDescription( key="kWh", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), "MWh": SensorEntityDescription( key="MWh", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, ), "h": SensorEntityDescription( key="h", diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json index 08a049cb17a..a863b9596b1 100644 --- a/homeassistant/components/nibe_heatpump/strings.json +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -2,8 +2,27 @@ "config": { "step": { "user": { + "menu_options": { + "nibegw": "NibeGW", + "modbus": "Modbus" + }, + "description": "Pick the connection method to your pump. In general, F-series pumps require a NibeGW custom accessory, while an S-series pump has Modbus support built-in." + }, + "modbus": { + "data": { + "model": "Model of Heat Pump", + "modbus_url": "Modbus URL", + "modbus_unit": "Modbus Unit Identifier" + }, + "data_description": { + "modbus_url": "Modbus URL that describes the connection to your Heat Pump or MODBUS40 unit. It should be on the form:\n - `tcp://[HOST]:[PORT]` for Modbus TCP connection\n - `serial://[LOCAL DEVICE]` for a local Modbus RTU connection\n - `rfc2217://[HOST]:[PORT]` for a remote telnet based Modbus RTU connection.", + "modbus_unit": "Unit identification for your Heat Pump. Can usually be left at 0." + } + }, + "nibegw": { "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory.", "data": { + "model": "Model of Heat Pump", "ip_address": "Remote address", "remote_read_port": "Remote read port", "remote_write_port": "Remote write port", @@ -18,12 +37,13 @@ } }, "error": { - "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`.", - "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", + "write": "Error on write request to pump. Verify your `Remote write port` or `Remote address`.", + "read": "Error on read request from pump. Verify your `Remote read port` or `Remote address`.", "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.", "address_in_use": "The selected listening port is already in use on this system.", - "model": "The model selected doesn't seem to support modbus40", - "unknown": "[%key:common::config_flow::error::unknown%]" + "model": "The selected model doesn't seem to support MODBUS40", + "unknown": "[%key:common::config_flow::error::unknown%]", + "url": "The specified URL is not well formed nor supported" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/bg.json b/homeassistant/components/nibe_heatpump/translations/bg.json index 88f52d84269..92456e60027 100644 --- a/homeassistant/components/nibe_heatpump/translations/bg.json +++ b/homeassistant/components/nibe_heatpump/translations/bg.json @@ -1,10 +1,14 @@ { "config": { - "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" - }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "menu_options": { + "nibegw": "NibeGW" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/ca.json b/homeassistant/components/nibe_heatpump/translations/ca.json index d2924212386..f489d017e86 100644 --- a/homeassistant/components/nibe_heatpump/translations/ca.json +++ b/homeassistant/components/nibe_heatpump/translations/ca.json @@ -1,21 +1,31 @@ { "config": { - "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" - }, "error": { "address": "Adre\u00e7a remota inv\u00e0lida. L'adre\u00e7a ha de ser una adre\u00e7a IP o un nom d'amfitri\u00f3 resoluble.", "address_in_use": "El port d'escolta seleccionat ja est\u00e0 en \u00fas en aquest sistema.", - "model": "El model seleccionat no sembla admetre modbus40", - "read": "Error en la sol\u00b7licitud de lectura de la bomba. Verifica el port remot de lectura i/o l'adre\u00e7a IP remota.", + "model": "El model seleccionat no sembla admetre MODBUS40", + "read": "Error en la sol\u00b7licitud de lectura de la bomba. Verifica el `port remot de lectura` i/o `l'adre\u00e7a remota`.", "unknown": "Error inesperat", - "write": "Error en la sol\u00b7licitud d'escriptura a la bomba. Verifica el port remot d'escriptura i/o l'adre\u00e7a IP remota." + "url": "L'URL especificat no est\u00e0 ben format o no \u00e9s compatible", + "write": "Error en la sol\u00b7licitud d'escriptura a la bomba. Verifica el `port remot d'escriptura` i/o `l'adre\u00e7a remota`." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Identificador d'unitat Modbus", + "modbus_url": "URL de Modbus", + "model": "Model de bomba de calor" + }, + "data_description": { + "modbus_unit": "Identificaci\u00f3 de la teva bomba de calor. Normalment es pot deixar a 0.", + "modbus_url": "URL de Modbus que descriu la connexi\u00f3 a la teva bomba de calor o unitat MODBUS40. Ha d'estar en el format:\n - `tcp://[HOST]:[PORT]` per a una connexi\u00f3 Modbus TCP\n - `serial://[DISPOSITIU LOCAL]` per a una connexi\u00f3 Modbus RTU local\n - `rfc2217://[HOST]:[PORT]` per a una connexi\u00f3 remota Modbus RTU basada en telnet." + } + }, + "nibegw": { "data": { "ip_address": "Adre\u00e7a remota", "listening_port": "Port local d'escolta", + "model": "Model de bomba de calor", "remote_read_port": "Port remot de lectura", "remote_write_port": "Port remot d'escriptura" }, @@ -26,6 +36,13 @@ "remote_write_port": "Port on la unitat NibeGW espera les sol\u00b7licituds d'escriptura." }, "description": "Abans d'intentar configurar la integraci\u00f3, comprova que:\n - La unitat NibeGW est\u00e0 connectada a una bomba de calor.\n - S'ha activat l'accessori MODBUS40 a la configuraci\u00f3 de la bomba de calor.\n - La bomba no ha entrat en estat d'alarma per falta de l'accessori MODBUS40." + }, + "user": { + "description": "Tria el m\u00e8tode de connexi\u00f3 a la teva bomba. En general, les bombes de la s\u00e8rie F necessiten un accessori personalitzat NibeGW, mentre que les bombes de la s\u00e8rie S tenen Modbus integrat.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/cs.json b/homeassistant/components/nibe_heatpump/translations/cs.json new file mode 100644 index 00000000000..5c7a625847f --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "url": "Zadan\u00e1 adresa URL nen\u00ed spr\u00e1vn\u011b zad\u00e1na ani podporov\u00e1na" + }, + "step": { + "modbus": { + "data": { + "modbus_url": "Modbus URL" + } + }, + "nibegw": { + "data": { + "ip_address": "Vzd\u00e1len\u00e1 adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/de.json b/homeassistant/components/nibe_heatpump/translations/de.json index 5cddee9d912..886513c0104 100644 --- a/homeassistant/components/nibe_heatpump/translations/de.json +++ b/homeassistant/components/nibe_heatpump/translations/de.json @@ -1,31 +1,48 @@ { "config": { - "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" - }, "error": { "address": "Ung\u00fcltige Remote-Adresse angegeben. Die Adresse muss eine IP-Adresse oder ein aufl\u00f6sbarer Hostname sein.", - "address_in_use": "Der ausgew\u00e4hlte Listening-Port wird auf diesem System bereits verwendet.", - "model": "Das ausgew\u00e4hlte Modell scheint modbus40 nicht zu unterst\u00fctzen", - "read": "Fehler bei Leseanforderung von Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Leseport\u201c oder \u201eRemote-IP-Adresse\u201c.", + "address_in_use": "Der ausgew\u00e4hlte Listening Port wird auf diesem System bereits verwendet.", + "model": "Das ausgew\u00e4hlte Modell scheint MODBUS40 nicht zu unterst\u00fctzen", + "read": "Fehler bei Leseanforderung von Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Leseport\u201c oder \u201eRemote-Adresse\u201c.", "unknown": "Unerwarteter Fehler", - "write": "Fehler bei Schreibanforderung an Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Schreibport\u201c oder \u201eRemote-IP-Adresse\u201c." + "url": "Die angegebene URL ist weder wohlgeformt, noch wird sie unterst\u00fctzt.", + "write": "Fehler bei Schreibanforderung an Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Schreibport\u201c oder \u201eRemote-Adresse\u201c." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus Einheitenkennung", + "modbus_url": "Modbus URL", + "model": "Modell der W\u00e4rmepumpe" + }, + "data_description": { + "modbus_unit": "Ger\u00e4teidentifikation f\u00fcr deine W\u00e4rmepumpe. Kann normalerweise auf 0 belassen werden.", + "modbus_url": "Modbus-URL, die die Verbindung zu deiner W\u00e4rmepumpe oder deinem MODBUS40-Ger\u00e4t beschreibt. Sie sollte in folgender Form sein:\n - `tcp://[HOST]:[PORT]` f\u00fcr eine Modbus TCP-Verbindung\n - `serial://[LOKALES GER\u00c4T]` f\u00fcr eine lokale Modbus RTU-Verbindung\n - `rfc2217://[HOST]:[PORT]` f\u00fcr eine Telnet-basierte Modbus-RTU-Fernverbindung." + } + }, + "nibegw": { "data": { "ip_address": "Remote-Adresse", "listening_port": "Lokaler Leseport", + "model": "Modell der W\u00e4rmepumpe", "remote_read_port": "Remote-Leseport", "remote_write_port": "Remote-Schreibport" }, "data_description": { - "ip_address": "Die Adresse des NibeGW-Ger\u00e4ts. Das Ger\u00e4t sollte mit einer statischen Adresse konfiguriert worden sein.", - "listening_port": "Der lokale Port auf diesem System, an den das NibeGW-Ger\u00e4t Daten senden soll.", - "remote_read_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Leseanfragen wartet.", - "remote_write_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Schreibanfragen wartet." + "ip_address": "Die Adresse des NibeGW Ger\u00e4ts. Das Ger\u00e4t sollte mit einer statischen Adresse konfiguriert worden sein.", + "listening_port": "Der lokale Port auf diesem System, an den das NibeGW Ger\u00e4t Daten senden soll.", + "remote_read_port": "Der Port, an dem das NibeGW Ger\u00e4t auf Leseanfragen wartet.", + "remote_write_port": "Der Port, an dem das NibeGW Ger\u00e4t auf Schreibanfragen wartet." }, - "description": "Bevor du versuchst, die Integration zu konfigurieren, \u00fcberpr\u00fcfe folgendes:\n - Das NibeGW-Ger\u00e4t ist an eine W\u00e4rmepumpe angeschlossen.\n - Das MODBUS40-Zubeh\u00f6r wurde in der Konfiguration der W\u00e4rmepumpe aktiviert.\n - Die Pumpe ist nicht in einen Alarmzustand wegen fehlendem MODBUS40-Zubeh\u00f6r \u00fcbergegangen." + "description": "Bevor du versuchst, die Integration zu konfigurieren, \u00fcberpr\u00fcfe folgendes:\n - Das NibeGW Ger\u00e4t ist an eine W\u00e4rmepumpe angeschlossen.\n - Das MODBUS40-Zubeh\u00f6r wurde in der Konfiguration der W\u00e4rmepumpe aktiviert.\n - Die Pumpe ist nicht in einen Alarmzustand wegen fehlendem MODBUS40-Zubeh\u00f6r \u00fcbergegangen." + }, + "user": { + "description": "W\u00e4hle die Verbindungsmethode zu deiner Pumpe. Im Allgemeinen erfordern Pumpen der F-Serie ein kundenspezifisches NibeGW- Zubeh\u00f6r, w\u00e4hrend eine Pumpe der S-Serie \u00fcber eine integrierte Modbus-Unterst\u00fctzung verf\u00fcgt.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/el.json b/homeassistant/components/nibe_heatpump/translations/el.json index a740bc43742..1933e062a09 100644 --- a/homeassistant/components/nibe_heatpump/translations/el.json +++ b/homeassistant/components/nibe_heatpump/translations/el.json @@ -1,23 +1,47 @@ { "config": { - "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" - }, "error": { "address": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPV4.", "address_in_use": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1.", "model": "\u03a4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 modbus40", "read": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\".", "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "url": "\u0397 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03bb\u03ac \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bf\u03cd\u03c4\u03b5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", "write": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\"." }, "step": { - "user": { + "modbus": { "data": { - "ip_address": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "modbus_unit": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 Modbus", + "modbus_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL Modbus", + "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b1\u03bd\u03c4\u03bb\u03af\u03b1\u03c2 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "data_description": { + "modbus_unit": "\u0391\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u03a3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bc\u03b5\u03af\u03bd\u03b5\u03b9 \u03c3\u03c4\u03bf 0.", + "modbus_url": "URL Modbus \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03ac\u03c6\u03b5\u03b9 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 MODBUS40. \u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03c6\u03cc\u03c1\u03bc\u03b1:\n - `tcp://[HOST]:[PORT]` \u03b3\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 Modbus TCP\n - `serial://[LOCAL DEVICE]` \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 Modbus RTU\n - `rfc2217://[HOST]:[PORT]` \u03b3\u03b9\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 Modbus RTU \u03c0\u03bf\u03c5 \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 telnet." + } + }, + "nibegw": { + "data": { + "ip_address": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7", "listening_port": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2", - "remote_read_port": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2", - "remote_write_port": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2" + "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b1\u03bd\u03c4\u03bb\u03af\u03b1\u03c2 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "remote_read_port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2", + "remote_write_port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2" + }, + "data_description": { + "ip_address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 NibeGW. \u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b8\u03b1 \u03ad\u03c0\u03c1\u03b5\u03c0\u03b5 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7.", + "listening_port": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2, \u03c3\u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 NibeGW \u03b3\u03b9\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd.", + "remote_read_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 NibeGW \u03b1\u03ba\u03bf\u03cd\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2.", + "remote_write_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 NibeGW \u03b1\u03ba\u03bf\u03cd\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2." + }, + "description": "\u03a0\u03c1\u03b9\u03bd \u03b5\u03c0\u03b9\u03c7\u03b5\u03b9\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9:\n - \u0397 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 NibeGW \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5 \u03b1\u03bd\u03c4\u03bb\u03af\u03b1 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2.\n - \u03a4\u03bf \u03b5\u03be\u03ac\u03c1\u03c4\u03b7\u03bc\u03b1 MODBUS40 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03bd\u03c4\u03bb\u03af\u03b1\u03c2 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2.\n - \u0397 \u03b1\u03bd\u03c4\u03bb\u03af\u03b1 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b5\u03b8\u03b5\u03af \u03c3\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd \u03b3\u03b9\u03b1 \u03ad\u03bb\u03bb\u03b5\u03b9\u03c8\u03b7 \u03b5\u03be\u03b1\u03c1\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 MODBUS40." + }, + "user": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1 \u03c3\u03b1\u03c2. \u0393\u03b5\u03bd\u03b9\u03ba\u03ac, \u03bf\u03b9 \u03b1\u03bd\u03c4\u03bb\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03ac\u03c2 F \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf \u03b5\u03be\u03ac\u03c1\u03c4\u03b7\u03bc\u03b1 NibeGW, \u03b5\u03bd\u03ce \u03bf\u03b9 \u03b1\u03bd\u03c4\u03bb\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03ac\u03c2 S \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03bc\u03ad\u03bd\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 Modbus.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json index 4c6e86720f1..5d0aaf4dd2d 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -1,21 +1,31 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.", "address_in_use": "The selected listening port is already in use on this system.", - "model": "The model selected doesn't seem to support modbus40", - "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", + "model": "The selected model doesn't seem to support MODBUS40", + "read": "Error on read request from pump. Verify your `Remote read port` or `Remote address`.", "unknown": "Unexpected error", - "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`." + "url": "The specified URL is not well formed nor supported", + "write": "Error on write request to pump. Verify your `Remote write port` or `Remote address`." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus Unit Identifier", + "modbus_url": "Modbus URL", + "model": "Model of Heat Pump" + }, + "data_description": { + "modbus_unit": "Unit identification for your Heat Pump. Can usually be left at 0.", + "modbus_url": "Modbus URL that describes the connection to your Heat Pump or MODBUS40 unit. It should be on the form:\n - `tcp://[HOST]:[PORT]` for Modbus TCP connection\n - `serial://[LOCAL DEVICE]` for a local Modbus RTU connection\n - `rfc2217://[HOST]:[PORT]` for a remote telnet based Modbus RTU connection." + } + }, + "nibegw": { "data": { "ip_address": "Remote address", "listening_port": "Local listening port", + "model": "Model of Heat Pump", "remote_read_port": "Remote read port", "remote_write_port": "Remote write port" }, @@ -26,6 +36,13 @@ "remote_write_port": "The port the NibeGW unit is listening for write requests on." }, "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory." + }, + "user": { + "description": "Pick the connection method to your pump. In general, F-series pumps require a NibeGW custom accessory, while an S-series pump has Modbus support built-in.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/es.json b/homeassistant/components/nibe_heatpump/translations/es.json index 0619471f538..fb5d35d209b 100644 --- a/homeassistant/components/nibe_heatpump/translations/es.json +++ b/homeassistant/components/nibe_heatpump/translations/es.json @@ -1,31 +1,48 @@ { "config": { - "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" - }, "error": { "address": "Se especific\u00f3 una direcci\u00f3n remota no v\u00e1lida. La direcci\u00f3n debe ser una direcci\u00f3n IP o un nombre de host resoluble.", "address_in_use": "El puerto de escucha seleccionado ya est\u00e1 en uso en este sistema.", - "model": "El modelo seleccionado no parece ser compatible con modbus40", - "read": "Error en la solicitud de lectura de la bomba. Verifica tu `Puerto de lectura remoto` o `Direcci\u00f3n IP remota`.", + "model": "El modelo seleccionado no parece ser compatible con MODBUS40", + "read": "Error en la solicitud de lectura de la bomba. Verifica tu `Puerto de lectura remoto` o `Direcci\u00f3n remota`.", "unknown": "Error inesperado", - "write": "Error en la solicitud de escritura a la bomba. Verifica tu `Puerto de escritura remoto` o `Direcci\u00f3n IP remota`." + "url": "La URL especificada no est\u00e1 bien formada ni es compatible", + "write": "Error en la solicitud de escritura a la bomba. Verifica tu `Puerto de escritura remoto` o `Direcci\u00f3n remota`." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Identificador de unidad Modbus", + "modbus_url": "URL Modbus", + "model": "Modelo de bomba de calor" + }, + "data_description": { + "modbus_unit": "Identificaci\u00f3n de la unidad para tu bomba de calor. Por lo general, se puede dejar en 0.", + "modbus_url": "URL Modbus que describe la conexi\u00f3n a tu bomba de calor o unidad MODBUS40. Debe estar en el formato:\n - `tcp://[HOST]:[PUERTO]` para conexi\u00f3n Modbus TCP\n - `serial://[DISPOSITIVO LOCAL]` para una conexi\u00f3n Modbus RTU local\n - `rfc2217://[HOST]:[PUERTO]` para una conexi\u00f3n remota Modbus RTU basada en telnet." + } + }, + "nibegw": { "data": { "ip_address": "Direcci\u00f3n remota", "listening_port": "Puerto de escucha local", + "model": "Modelo de bomba de calor", "remote_read_port": "Puerto de lectura remoto", "remote_write_port": "Puerto de escritura remoto" }, "data_description": { "ip_address": "La direcci\u00f3n de la unidad NibeGW. El dispositivo deber\u00eda haber sido configurado con una direcci\u00f3n est\u00e1tica.", "listening_port": "El puerto local en este sistema, al que la unidad NibeGW est\u00e1 configurada para enviar datos.", - "remote_read_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando las peticiones de lectura.", + "remote_read_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando peticiones de lectura.", "remote_write_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando peticiones de escritura." }, - "description": "Antes de intentar configurar la integraci\u00f3n, verifica que:\n- La unidad NibeGW est\u00e1 conectada a una bomba de calor.\n- Se ha habilitado el accesorio MODBUS40 en la configuraci\u00f3n de la bomba de calor.\n- La bomba no ha entrado en estado de alarma por falta del accesorio MODBUS40." + "description": "Antes de intentar configurar la integraci\u00f3n, verifica que:\n - La unidad NibeGW est\u00e1 conectada a una bomba de calor.\n - Se ha habilitado el accesorio MODBUS40 en la configuraci\u00f3n de la bomba de calor.\n - La bomba no ha entrado en estado de alarma por falta del accesorio MODBUS40." + }, + "user": { + "description": "Elige el m\u00e9todo de conexi\u00f3n a tu bomba. En general, las bombas de la serie F requieren un accesorio personalizado NibeGW, mientras que una bomba de la serie S tiene soporte Modbus incorporado.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/et.json b/homeassistant/components/nibe_heatpump/translations/et.json index 223d0f22c1a..1876e93305a 100644 --- a/homeassistant/components/nibe_heatpump/translations/et.json +++ b/homeassistant/components/nibe_heatpump/translations/et.json @@ -1,21 +1,31 @@ { "config": { - "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" - }, "error": { "address": "M\u00e4\u00e4ratud vale kaugaadress. Aadress peab olema IP-aadress v\u00f5i lahendatav hostinimi.", "address_in_use": "Valitud kuulamisport on selles s\u00fcsteemis juba kasutusel.", - "model": "Valitud mudel ei n\u00e4i toetavat modbus40.", - "read": "Viga pumba lugemistaotlusel. Kinnitage oma \"Kaugloetav port\" v\u00f5i \"Kaug-IP-aadress\".", + "model": "Valitud mudel ei n\u00e4i toetavat MODBUS40", + "read": "Viga pumba lugemistaotlusel. Kinnita oma \"Kaugloetav port\" v\u00f5i \"Kaug-IP-aadress\".", "unknown": "Ootamatu t\u00f5rge", + "url": "M\u00e4\u00e4ratud URL ei ole h\u00e4sti vormindatud ja toetatud", "write": "Viga pumba kirjutamise taotlusel. Kontrollige oma `kaugkirjutusport` v\u00f5i `kaug-IP-aadress`." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus-i \u00fcksuse identifikaator", + "modbus_url": "Modbus-i URL", + "model": "Soojuspumba mudel" + }, + "data_description": { + "modbus_unit": "Soojuspumba seadme identifitseerimine. Tavaliselt v\u00f5ib j\u00e4tta 0-le.", + "modbus_url": "Modbusi URL mis kirjeldab \u00fchendust soojuspumba v\u00f5i MODBUS40 seadmega. See peaks olema vormis:\n - `tcp://[HOST]:[PORT]` Modbusi TCP-\u00fchenduse jaoks\n - \"serial://[LOCAL DEVICE]\" kohaliku Modbus RTU \u00fchenduse jaoks\n - `rfc2217://[HOST]:[PORT]` telnetip\u00f5hise Modbus RTU kaug\u00fchenduse jaoks." + } + }, + "nibegw": { "data": { "ip_address": "Kaug-IP-aadress", "listening_port": "Kohalik kuulamisport", + "model": "Soojuspumba mudel", "remote_read_port": "Kauglugemise port", "remote_write_port": "Kaugkirjutusport" }, @@ -25,7 +35,14 @@ "remote_read_port": "Port, mille kaudu NibeGW-\u00fcksus loeb lugemisp\u00e4ringuid.", "remote_write_port": "Port, mille kaudu NibeGW-\u00fcksus kuulab kirjutamisp\u00e4ringuid." }, - "description": "Enne sidumise seadistamist veendu, et:\n - NibeGW seade on \u00fchendatud soojuspumbaga.\n - MODBUS40 tarvik on soojuspumba konfiguratsioonis lubatud.\n - Pump ei ole MODBUS40 lisaseadme puudumise t\u00f5ttu h\u00e4ireolekusse l\u00e4inud." + "description": "Enne seadistamist veendu, et:\n - NibeGW seade on \u00fchendatud soojuspumbaga.\n - MODBUS40 lisaseade on soojuspumba konfiguratsioonis lubatud.\n - Pump ei ole MODBUS40 lisaseadme puudumise t\u00f5ttu h\u00e4ireolekusse l\u00e4inud." + }, + "user": { + "description": "Vali pumbaga \u00fchendamise viis. \u00dcldiselt vajavad F-seeria pumbad Nibe GW kohandatud tarvikut, S-seeria pumbal on aga sisseehitatud Modbusi tugi.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/fr.json b/homeassistant/components/nibe_heatpump/translations/fr.json index 6c12361adc5..799ee8c2b73 100644 --- a/homeassistant/components/nibe_heatpump/translations/fr.json +++ b/homeassistant/components/nibe_heatpump/translations/fr.json @@ -1,18 +1,30 @@ { "config": { - "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" - }, "error": { + "model": "Le mod\u00e8le s\u00e9lectionn\u00e9 ne semble pas prendre en charge MODBUS40", "unknown": "Erreur inattendue" }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Identifiant d\u2019unit\u00e9 Modbus", + "modbus_url": "URL Modbus", + "model": "Mod\u00e8le de la pompe \u00e0 chaleur" + } + }, + "nibegw": { "data": { "ip_address": "Adresse distante", - "listening_port": "Port d'\u00e9coute local", + "listening_port": "Port d\u2019\u00e9coute local", + "model": "Mod\u00e8le de la pompe \u00e0 chaleur", "remote_read_port": "Port de lecture distant", - "remote_write_port": "Port d'\u00e9criture distant" + "remote_write_port": "Port d\u2019\u00e9criture distant" + } + }, + "user": { + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/he.json b/homeassistant/components/nibe_heatpump/translations/he.json index ea40181bd9a..822dcf2be14 100644 --- a/homeassistant/components/nibe_heatpump/translations/he.json +++ b/homeassistant/components/nibe_heatpump/translations/he.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" - }, "error": { "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" } diff --git a/homeassistant/components/nibe_heatpump/translations/hu.json b/homeassistant/components/nibe_heatpump/translations/hu.json index 1dc8ea12179..3f6c9845656 100644 --- a/homeassistant/components/nibe_heatpump/translations/hu.json +++ b/homeassistant/components/nibe_heatpump/translations/hu.json @@ -1,31 +1,48 @@ { "config": { - "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" - }, "error": { "address": "\u00c9rv\u00e9nytelen t\u00e1voli c\u00edm van megadva. A c\u00edmnek IP-c\u00edmnek vagy feloldhat\u00f3 g\u00e9pn\u00e9vnek kell lennie.", "address_in_use": "A kiv\u00e1lasztott port m\u00e1r haszn\u00e1latban van ezen a rendszeren.", "model": "\u00dagy t\u0171nik, hogy a kiv\u00e1lasztott modell nem t\u00e1mogatja a modbus40-et", "read": "Hiba a szivatty\u00fa olvas\u00e1si k\u00e9r\u00e9s\u00e9n\u00e9l. Ellen\u0151rizze a \"T\u00e1voli olvas\u00e1si portot\" vagy a \"T\u00e1voli IP-c\u00edmet\".", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "url": "A megadott URL-c\u00edm nem j\u00f3l form\u00e1zott \u00e9s t\u00e1mogatott URL-c\u00edm", "write": "Hiba a h\u0151szivatty\u00fa \u00edr\u00e1si k\u00e9relm\u00e9ben. Ellen\u0151rizze a portot, c\u00edmet." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus egys\u00e9g azonos\u00edt\u00f3ja", + "modbus_url": "Modbus URL", + "model": "A h\u0151szivatty\u00fa modellje" + }, + "data_description": { + "modbus_unit": "A h\u0151szivatty\u00fa egys\u00e9gazonos\u00edt\u00e1sa. \u00c1ltal\u00e1ban 0 \u00e9rt\u00e9ken hagyhat\u00f3.", + "modbus_url": "Modbus URL, amely le\u00edrja a h\u0151szivatty\u00faval vagy a MODBUS40 egys\u00e9ggel val\u00f3 kapcsolatot. Az \u0171rlapon kell lennie:\n - 'tcp://[HOST]:[PORT]' a Modbus TCP kapcsolathoz\n - \"serial://[HELYI ESZK\u00d6Z]\" helyi Modbus RTU kapcsolathoz\n - 'rfc2217://[HOST]:[PORT]' t\u00e1voli telnet alap\u00fa Modbus RTU kapcsolathoz." + } + }, + "nibegw": { "data": { "ip_address": "T\u00e1voli IP-c\u00edm", "listening_port": "Helyi port", + "model": "A h\u0151szivatty\u00fa modellje", "remote_read_port": "T\u00e1voli olvas\u00e1si port", "remote_write_port": "T\u00e1voli \u00edr\u00e1si port" }, "data_description": { "ip_address": "A NibeGW egys\u00e9g c\u00edme. A k\u00e9sz\u00fcl\u00e9ket statikus c\u00edmmel kell konfigur\u00e1lni.", - "listening_port": "A rendszer azon helyi portja, amelyre a NibeGW egys\u00e9g az adatok k\u00fcld\u00e9s\u00e9re van konfigur\u00e1lva.", - "remote_read_port": "A port, amelyen a NibeGW egys\u00e9g olvas\u00e1si k\u00e9r\u00e9seket fogad.", - "remote_write_port": "A port, amelyen a NibeGW egys\u00e9g \u00edr\u00e1si k\u00e9r\u00e9seket fogad." + "listening_port": "A rendszer helyi portja, amelyre a NibeGW egys\u00e9g \u00fagy van konfigur\u00e1lva, hogy adatokat k\u00fcldj\u00f6n.", + "remote_read_port": "A port, amelyen a NibeGW egys\u00e9g olvas\u00e1si k\u00e9r\u00e9seket v\u00e1r.", + "remote_write_port": "A port, amelyen a NibeGW egys\u00e9g az \u00edr\u00e1si k\u00e9r\u00e9seket v\u00e1rja." }, "description": "Miel\u0151tt megpr\u00f3b\u00e1ln\u00e1 konfigur\u00e1lni az integr\u00e1ci\u00f3t, ellen\u0151rizze, hogy:\n - A NibeGW egys\u00e9g h\u0151szivatty\u00fahoz van csatlakoztatva.\n - A MODBUS40 kieg\u00e9sz\u00edt\u0151 enged\u00e9lyezve van a h\u0151szivatty\u00fa konfigur\u00e1ci\u00f3j\u00e1ban.\n - A szivatty\u00fa nem l\u00e9pett riaszt\u00e1si \u00e1llapotba a MODBUS40 tartoz\u00e9k hi\u00e1nya miatt." + }, + "user": { + "description": "V\u00e1lassza ki a szivatty\u00fahoz val\u00f3 csatlakoz\u00e1si m\u00f3dot. Az F-sorozat\u00fa szivatty\u00fakhoz \u00e1ltal\u00e1ban Nibe GW egyedi tartoz\u00e9kra van sz\u00fcks\u00e9g, m\u00edg az S-sorozat\u00fa szivatty\u00fak be\u00e9p\u00edtett Modbus-t\u00e1mogat\u00e1ssal rendelkeznek.", + "menu_options": { + "modbus": "ModBUS", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/id.json b/homeassistant/components/nibe_heatpump/translations/id.json index 53e3d202877..4e9e7c181d0 100644 --- a/homeassistant/components/nibe_heatpump/translations/id.json +++ b/homeassistant/components/nibe_heatpump/translations/id.json @@ -1,23 +1,47 @@ { "config": { - "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" - }, "error": { - "address": "Alamat IP jarak jauh yang ditentukan tidak valid. Alamat harus berupa alamat IPv4.", + "address": "Alamat IP jarak jauh yang ditentukan tidak valid. Alamat harus berupa alamat IP atau nama host yang dapat ditemukan.", "address_in_use": "Port mendengarkan yang dipilih sudah digunakan pada sistem ini.", - "model": "Model yang dipilih tampaknya tidak mendukung modbus40", - "read": "Kesalahan pada permintaan baca dari pompa. Verifikasi `Port baca jarak jauh` atau `Alamat IP jarak jauh` Anda.", + "model": "Model yang dipilih tampaknya tidak mendukung MODBUS40", + "read": "Kesalahan pada permintaan baca dari pompa. Verifikasi `Port baca jarak jauh` atau `Alamat jarak jauh` Anda.", "unknown": "Kesalahan yang tidak diharapkan", - "write": "Kesalahan pada permintaan tulis ke pompa. Verifikasi `Port tulis jarak jauh` atau `Alamat IP jarak jauh` Anda." + "url": "URL yang ditentukan tidak dalam format yang benar atau tidak didukung", + "write": "Kesalahan pada permintaan tulis ke pompa. Verifikasi `Port tulis jarak jauh` atau `Alamat jarak jauh` Anda." }, "step": { - "user": { + "modbus": { "data": { - "ip_address": "Alamat IP jarak jauh", + "modbus_unit": "Pengidentifikasi Unit Modbus", + "modbus_url": "URL Modbus", + "model": "Model Pompa Panas" + }, + "data_description": { + "modbus_unit": "Identifikasi unit untuk Pompa Panas Anda. Biasanya dapat dibiarkan pada nilai 0.", + "modbus_url": "URL Modbus yang menjelaskan koneksi ke unit Heat Pump atau MODBUS40 Anda. Ini harus dalam format:\n - `tcp://[HOST]:[PORT]` untuk koneksi Modbus TCP\n - `serial://[LOCAL DEVICE]` untuk koneksi Modbus RTU lokal\n - `rfc2217://[HOST]:[PORT]` untuk koneksi Modbus RTU berbasis telnet jarak jauh." + } + }, + "nibegw": { + "data": { + "ip_address": "Alamat jarak jauh", "listening_port": "Port mendengarkan lokal", + "model": "Model Pompa Panas", "remote_read_port": "Port baca jarak jauh", "remote_write_port": "Port tulis jarak jauh" + }, + "data_description": { + "ip_address": "Alamat unit NibeGW. Perangkat seharusnya sudah dikonfigurasi dengan alamat statis.", + "listening_port": "Port lokal pada sistem ini, yang dikonfigurasi untuk mengirim data ke unit NibeGW.", + "remote_read_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan baca.", + "remote_write_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan tulis." + }, + "description": "Sebelum mencoba mengonfigurasi integrasi, pastikan bahwa:\n - Unit NibeGW terhubung ke pompa panas.\n - Aksesori MODBUS40 telah diaktifkan dalam konfigurasi pompa panas.\n - Pompa tidak sedang dalam status alarm tentang aksesori MODBUS40 yang tidak tersedia." + }, + "user": { + "description": "Pilih metode koneksi ke pompa. Secara umum, pompa seri F memerlukan aksesori khusus NibeGW, sementara pompa seri S memiliki dukungan Modbus bawaan.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/it.json b/homeassistant/components/nibe_heatpump/translations/it.json index 9de61113160..dd629388f0d 100644 --- a/homeassistant/components/nibe_heatpump/translations/it.json +++ b/homeassistant/components/nibe_heatpump/translations/it.json @@ -1,23 +1,47 @@ { "config": { - "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" - }, "error": { - "address": "Indirizzo IP remoto specificato non valido. L'indirizzo deve essere un indirizzo IPV4.", + "address": "Indirizzo remoto specificato non valido. L'indirizzo deve essere un indirizzo IP o un nome host risolvibile.", "address_in_use": "La porta di ascolto selezionata \u00e8 gi\u00e0 in uso su questo sistema.", - "model": "Il modello selezionato non sembra supportare il modbus40", - "read": "Errore su richiesta di lettura dalla pompa. Verifica la tua \"Porta di lettura remota\" o \"Indirizzo IP remoto\".", + "model": "Il modello selezionato non sembra supportare il MODBUS40.", + "read": "Errore nella richiesta di lettura da parte della pompa. Verifica la tua \"Porta di lettura remota\" o \"Indirizzo remoto\".", "unknown": "Errore imprevisto", - "write": "Errore nella richiesta di scrittura alla pompa. Verifica la tua \"Porta di scrittura remota\" o \"Indirizzo IP remoto\"." + "url": "L'URL specificato non \u00e8 correttamente formato n\u00e9 supportato", + "write": "Errore nella richiesta di scrittura alla pompa. Verifica la tua \"Porta di scrittura remota\" o \"Indirizzo remoto\"." }, "step": { - "user": { + "modbus": { "data": { - "ip_address": "Indirizzo IP remoto", + "modbus_unit": "Identificatore unit\u00e0 Modbus", + "modbus_url": "Modbus URL", + "model": "Modello di pompa di calore" + }, + "data_description": { + "modbus_unit": "Identificazione dell'unit\u00e0 per la pompa di calore. Di solito pu\u00f2 essere lasciato a 0.", + "modbus_url": "Modbus URL che descrive la connessione alla pompa di calore o all'unit\u00e0 MODBUS40. Dovrebbe essere nella forma:\n - `tcp://[HOST]:[PORTA]` per la connessione Modbus TCP\n - `serial://[DISPOSITIVO LOCALE]` per una connessione Modbus RTU locale\n - `rfc2217://[HOST]:[PORTA]` per una connessione Modbus RTU remota basata su telnet." + } + }, + "nibegw": { + "data": { + "ip_address": "Indirizzo remoto", "listening_port": "Porta di ascolto locale", + "model": "Modello di pompa di calore", "remote_read_port": "Porta di lettura remota", "remote_write_port": "Porta di scrittura remota" + }, + "data_description": { + "ip_address": "L'indirizzo dell'unit\u00e0 NibeGW. Il dispositivo dovrebbe essere stato configurato con un indirizzo statico.", + "listening_port": "La porta locale su questo sistema a cui l'unit\u00e0 NibeGW \u00e8 configurata per inviare i dati.", + "remote_read_port": "La porta su cui l'unit\u00e0 NibeGW \u00e8 in ascolto per le richieste di lettura.", + "remote_write_port": "La porta su cui l'unit\u00e0 NibeGW \u00e8 in ascolto per le richieste di scrittura." + }, + "description": "Prima di tentare di configurare l'integrazione, verificare che:\n - L'unit\u00e0 NibeGW \u00e8 collegata a una pompa di calore.\n - Nella configurazione della pompa di calore \u00e8 stato abilitato l'accessorio MODBUS40.\n - La pompa non \u00e8 andata in stato di allarme per la mancanza dell'accessorio MODBUS40." + }, + "user": { + "description": "Scegli il metodo di connessione alla tua pompa. In generale, le pompe della serie F richiedono un accessorio personalizzato NibeGW, mentre una pompa della serie S ha il supporto Modbus integrato.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/ja.json b/homeassistant/components/nibe_heatpump/translations/ja.json index 6ca4ad37a81..9ad7fd4a7aa 100644 --- a/homeassistant/components/nibe_heatpump/translations/ja.json +++ b/homeassistant/components/nibe_heatpump/translations/ja.json @@ -1,20 +1,7 @@ { "config": { - "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" - }, "error": { "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "step": { - "user": { - "data": { - "ip_address": "\u30ea\u30e2\u30fc\u30c8IP\u30a2\u30c9\u30ec\u30b9", - "listening_port": "\u30ed\u30fc\u30ab\u30eb\u30ea\u30b9\u30cb\u30f3\u30b0\u30dd\u30fc\u30c8", - "remote_read_port": "\u30ea\u30e2\u30fc\u30c8\u8aad\u307f\u53d6\u308a\u30dd\u30fc\u30c8", - "remote_write_port": "\u30ea\u30e2\u30fc\u30c8\u66f8\u304d\u8fbc\u307f\u30dd\u30fc\u30c8" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/nb.json b/homeassistant/components/nibe_heatpump/translations/nb.json index 2e302a80d31..a22f7eef3d6 100644 --- a/homeassistant/components/nibe_heatpump/translations/nb.json +++ b/homeassistant/components/nibe_heatpump/translations/nb.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Enheten er allerede konfigurert" - }, "error": { "unknown": "Uventet feil" } diff --git a/homeassistant/components/nibe_heatpump/translations/nl.json b/homeassistant/components/nibe_heatpump/translations/nl.json index c227699ff21..7e198e836d7 100644 --- a/homeassistant/components/nibe_heatpump/translations/nl.json +++ b/homeassistant/components/nibe_heatpump/translations/nl.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Apparaat is al geconfigureerd" - }, "error": { "unknown": "Onverwachte fout" } diff --git a/homeassistant/components/nibe_heatpump/translations/no.json b/homeassistant/components/nibe_heatpump/translations/no.json index b0a8f601776..bc0963c6a64 100644 --- a/homeassistant/components/nibe_heatpump/translations/no.json +++ b/homeassistant/components/nibe_heatpump/translations/no.json @@ -1,21 +1,31 @@ { "config": { - "abort": { - "already_configured": "Enheten er allerede konfigurert" - }, "error": { "address": "Ugyldig ekstern adresse er angitt. Adressen m\u00e5 v\u00e6re en IP-adresse eller et vertsnavn som kan l\u00f8ses.", "address_in_use": "Den valgte lytteporten er allerede i bruk p\u00e5 dette systemet.", - "model": "Den valgte modellen ser ikke ut til \u00e5 st\u00f8tte modbus40", - "read": "Feil ved leseforesp\u00f8rsel fra pumpe. Bekreft din \"Ekstern leseport\" eller \"Ekstern IP-adresse\".", + "model": "Den valgte modellen ser ikke ut til \u00e5 st\u00f8tte MODBUS40", + "read": "Feil ved leseforesp\u00f8rsel fra pumpe. Bekreft din \"Ekstern leseport\" eller \"Ekstern adresse\".", "unknown": "Uventet feil", - "write": "Feil ved skriveforesp\u00f8rsel til pumpen. Bekreft din \"Ekstern skriveport\" eller \"Ekstern IP-adresse\"." + "url": "Den angitte URL-en er ikke godt utformet eller st\u00f8ttet", + "write": "Feil ved skriveforesp\u00f8rsel til pumpen. Bekreft din \"Ekstern skriveport\" eller \"Ekstern adresse\"." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus Unit Identifier", + "modbus_url": "Modbus URL", + "model": "Modell av varmepumpe" + }, + "data_description": { + "modbus_unit": "Enhetsidentifikasjon for din varmepumpe. Kan vanligvis st\u00e5 p\u00e5 0.", + "modbus_url": "Modbus URL som beskriver tilkoblingen til din varmepumpe eller MODBUS40 enhet. Det skal st\u00e5 p\u00e5 skjemaet:\n - `tcp://[HOST]:[PORT]` for Modbus TCP-tilkobling\n - `serial://[LOCAL DEVICE]` for en lokal Modbus RTU-tilkobling\n - `rfc2217://[HOST]:[PORT]` for en ekstern telnet-basert Modbus RTU-tilkobling." + } + }, + "nibegw": { "data": { "ip_address": "Ekstern adresse", "listening_port": "Lokal lytteport", + "model": "Modell av varmepumpe", "remote_read_port": "Ekstern leseport", "remote_write_port": "Ekstern skriveport" }, @@ -26,6 +36,13 @@ "remote_write_port": "Porten NibeGW-enheten lytter etter skriveforesp\u00f8rsler p\u00e5." }, "description": "F\u00f8r du pr\u00f8ver \u00e5 konfigurere integrasjonen, kontroller at:\n - NibeGW-enheten er koblet til en varmepumpe.\n - MODBUS40-tilbeh\u00f8ret er aktivert i varmepumpekonfigurasjonen.\n - Pumpen har ikke g\u00e5tt i alarmtilstand om manglende MODBUS40-tilbeh\u00f8r." + }, + "user": { + "description": "Velg tilkoblingsmetoden til pumpen din. Generelt krever pumper i F-serien et NibeGW-tilpasset tilbeh\u00f8r, mens en pumpe i S-serien har Modbus-st\u00f8tte innebygd.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/pl.json b/homeassistant/components/nibe_heatpump/translations/pl.json index 8298b41c64c..0e66092c5ff 100644 --- a/homeassistant/components/nibe_heatpump/translations/pl.json +++ b/homeassistant/components/nibe_heatpump/translations/pl.json @@ -1,21 +1,31 @@ { "config": { - "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" - }, "error": { "address": "Podano nieprawid\u0142owy zdalny adres IP. Adres musi by\u0107 adresem IP lub rozpoznawaln\u0105 nazw\u0105 hosta.", "address_in_use": "Wybrany port nas\u0142uchiwania jest ju\u017c u\u017cywany w tym systemie.", - "model": "Wybrany model nie obs\u0142uguje modbus40", + "model": "Wybrany model nie obs\u0142uguje MODBUS40", "read": "B\u0142\u0105d przy \u017c\u0105daniu odczytu z pompy. Sprawd\u017a \u201eZdalny port odczytu\u201d lub \u201eZdalny adres IP\u201d.", "unknown": "Nieoczekiwany b\u0142\u0105d", + "url": "Podany adres URL nie jest poprawnie sformu\u0142owany lub obs\u0142ugiwany", "write": "B\u0142\u0105d przy \u017c\u0105daniu zapisu do pompy. Sprawd\u017a \u201eZdalny port zapisu\u201d lub \u201eZdalny adres IP\u201d." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Identyfikator jednostki Modbus", + "modbus_url": "Adres URL Modbus", + "model": "Model pompy ciep\u0142a" + }, + "data_description": { + "modbus_unit": "Identyfikacja jednostki pompa ciep\u0142a. Zwykle mo\u017cna pozostawi\u0107 0.", + "modbus_url": "Adres URL Modbus opisuj\u0105cy po\u0142\u0105czenie z pomp\u0105 ciep\u0142a lub jednostk\u0105 MODBUS40. Powinien by\u0107 w formacie:\n- `tcp://[HOST]:[PORT]` dla po\u0142\u0105czenia Modbus TCP\n- `serial://[LOCAL DEVICE]` dla lokalnego po\u0142\u0105czenia Modbus RTU\n- `rfc2217://[HOST]:[PORT]` dla zdalnego po\u0142\u0105czenia Modbus RTU opartego na telnet." + } + }, + "nibegw": { "data": { "ip_address": "Zdalny adres IP", "listening_port": "Lokalny port nas\u0142uchiwania", + "model": "Model pompy ciep\u0142a", "remote_read_port": "Zdalny port odczytu", "remote_write_port": "Zdalny port zapisu" }, @@ -26,6 +36,13 @@ "remote_write_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 zapisu." }, "description": "Przed przyst\u0105pieniem do konfiguracji integracji sprawd\u017a, czy:\n - Urz\u0105dzenie NibeGW jest pod\u0142\u0105czona do pompy ciep\u0142a.\n - Akcesorium MODBUS40 zosta\u0142o w\u0142\u0105czone w konfiguracji pompy ciep\u0142a.\n - Pompa nie wesz\u0142a w stan alarmowy z powodu braku akcesorium MODBUS40." + }, + "user": { + "description": "Wybierz metod\u0119 po\u0142\u0105czenia z pomp\u0105. Og\u00f3lnie rzecz bior\u0105c, pompy serii F wymagaj\u0105 niestandardowego akcesorium NibeGW, podczas gdy pompy serii S maj\u0105 wbudowan\u0105 obs\u0142ug\u0119 protoko\u0142u Modbus.", + "menu_options": { + "modbus": "MODBUS", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/pt-BR.json b/homeassistant/components/nibe_heatpump/translations/pt-BR.json index 9f999846036..c3e97bd3e3d 100644 --- a/homeassistant/components/nibe_heatpump/translations/pt-BR.json +++ b/homeassistant/components/nibe_heatpump/translations/pt-BR.json @@ -1,21 +1,31 @@ { "config": { - "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" - }, "error": { "address": "Endere\u00e7o remoto inv\u00e1lido especificado. O endere\u00e7o deve ser um endere\u00e7o IP ou um nome de host resolv\u00edvel.", "address_in_use": "A porta de escuta selecionada j\u00e1 est\u00e1 em uso neste sistema.", - "model": "O modelo selecionado parece n\u00e3o suportar modbus40", - "read": "Erro na solicita\u00e7\u00e3o de leitura da bomba. Verifique sua `Porta de leitura remota` ou `Endere\u00e7o IP remoto`.", + "model": "O modelo selecionado parece n\u00e3o suportar MODBUS40", + "read": "Erro na solicita\u00e7\u00e3o de leitura da bomba. Verifique sua `Porta de leitura remota` ou `Endere\u00e7o remoto`.", "unknown": "Erro inesperado", - "write": "Erro na solicita\u00e7\u00e3o de grava\u00e7\u00e3o para bombear. Verifique sua `Porta de grava\u00e7\u00e3o remota` ou `Endere\u00e7o IP remoto`." + "url": "A URL especificada n\u00e3o \u00e9 uma URL bem formada e suportada", + "write": "Erro na solicita\u00e7\u00e3o de grava\u00e7\u00e3o para bombear. Verifique sua `Porta de grava\u00e7\u00e3o remota` ou `Endere\u00e7o remoto`." }, "step": { - "user": { + "modbus": { "data": { - "ip_address": "Endere\u00e7o IP remoto", + "modbus_unit": "Identificador da Unidade Modbus", + "modbus_url": "URL de Modbus", + "model": "Modelo de Bomba de Calor" + }, + "data_description": { + "modbus_unit": "Identifica\u00e7\u00e3o da unidade para a sua Bomba de Calor. Geralmente pode ser deixado em 0.", + "modbus_url": "URL de Modbus que descreve a liga\u00e7\u00e3o \u00e0 sua Bomba de Calor ou unidade MODBUS40. Deve estar no formul\u00e1rio:\n - `tcp://[HOST]:[PORT]` para conex\u00e3o Modbus TCP\n - `serial://[LOCAL DEVICE]` para uma conex\u00e3o Modbus RTU local\n - `rfc2217://[HOST]:[PORT]` para uma conex\u00e3o Modbus RTU baseada em telnet remoto." + } + }, + "nibegw": { + "data": { + "ip_address": "Endere\u00e7o remoto", "listening_port": "Porta de escuta local", + "model": "Modelo de Bomba de Calor", "remote_read_port": "Porta de leitura remota", "remote_write_port": "Porta de grava\u00e7\u00e3o remota" }, @@ -26,6 +36,13 @@ "remote_write_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de grava\u00e7\u00e3o." }, "description": "Antes de tentar configurar a integra\u00e7\u00e3o, verifique se:\n - A unidade NibeGW est\u00e1 conectada a uma bomba de calor.\n - O acess\u00f3rio MODBUS40 foi habilitado na configura\u00e7\u00e3o da bomba de calor.\n - A bomba n\u00e3o entrou em estado de alarme por falta de acess\u00f3rio MODBUS40." + }, + "user": { + "description": "Escolha o m\u00e9todo de conex\u00e3o para sua bomba. Em geral, as bombas da s\u00e9rie F requerem um acess\u00f3rio personalizado NibeGW, enquanto uma bomba da s\u00e9rie S tem suporte Modbus integrado.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/ru.json b/homeassistant/components/nibe_heatpump/translations/ru.json index e1192c7e08c..d4b8a2608ee 100644 --- a/homeassistant/components/nibe_heatpump/translations/ru.json +++ b/homeassistant/components/nibe_heatpump/translations/ru.json @@ -1,23 +1,47 @@ { "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": { - "address": "\u0423\u043a\u0430\u0437\u0430\u043d \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 IPV4.", + "address": "\u0423\u043a\u0430\u0437\u0430\u043d \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441. \u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430.", "address_in_use": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u044d\u0442\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", - "model": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 modbus40.", - "read": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`.", + "model": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 MODBUS40.", + "read": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f` \u0438\u043b\u0438 `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \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.", - "write": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`." + "url": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0441\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "write": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438` \u0438\u043b\u0438 `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441`." }, "step": { - "user": { + "modbus": { "data": { - "ip_address": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441", + "modbus_unit": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u043e\u0434\u0443\u043b\u044f Modbus", + "modbus_url": "URL-\u0430\u0434\u0440\u0435\u0441 Modbus", + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430" + }, + "data_description": { + "modbus_unit": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430. \u041e\u0431\u044b\u0447\u043d\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430 0.", + "modbus_url": "URL-\u0430\u0434\u0440\u0435\u0441 Modbus, \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443 \u0438\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 MODBUS40. \u041e\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435:\n- `tcp://[HOST]:[PORT]` \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Modbus TCP\n- `serial://[LOCAL DEVICE]` \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f Modbus RTU\n- `rfc2217://[HOST]:[PORT]` \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Modbus RTU \u0447\u0435\u0440\u0435\u0437 telnet." + } + }, + "nibegw": { + "data": { + "ip_address": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441", "listening_port": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f", + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430", "remote_read_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f", "remote_write_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438" + }, + "data_description": { + "ip_address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 NibeGW. \u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u0441\u043e \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c.", + "listening_port": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0432 \u044d\u0442\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445.", + "remote_read_port": "\u041f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435.", + "remote_write_port": "\u041f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c." + }, + "description": "\u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u0442\u044c \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e:\n - \u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443.\n - \u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 MODBUS40 \u0431\u044b\u043b \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430.\n - \u041d\u0430\u0441\u043e\u0441 \u043d\u0435 \u043f\u0435\u0440\u0435\u0448\u0435\u043b \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0442\u0440\u0435\u0432\u043e\u0433\u0438 \u0438\u0437 \u0437\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 MODBUS40." + }, + "user": { + "description": "\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 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443. \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0434\u043b\u044f \u043d\u0430\u0441\u043e\u0441\u043e\u0432 \u0441\u0435\u0440\u0438\u0438 F \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 NibeGW, \u0432 \u0442\u043e \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043d\u0430\u0441\u043e\u0441\u044b \u0441\u0435\u0440\u0438\u0438 S \u0438\u043c\u0435\u044e\u0442 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 Modbus.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/sk.json b/homeassistant/components/nibe_heatpump/translations/sk.json new file mode 100644 index 00000000000..50d472b9c96 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/sk.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "address_in_use": "Vybran\u00fd port po\u010d\u00favania sa u\u017e v tomto syst\u00e9me pou\u017e\u00edva.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "modbus": { + "data": { + "modbus_url": "Modbus URL", + "model": "Model tepeln\u00e9ho \u010derpadla" + } + }, + "nibegw": { + "data": { + "ip_address": "Vzdialen\u00e1 adresa", + "model": "Model tepeln\u00e9ho \u010derpadla", + "remote_read_port": "Port pre vzdialen\u00e9 \u010d\u00edtanie", + "remote_write_port": "Port pre vzdialen\u00fd z\u00e1pis" + } + }, + "user": { + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/sv.json b/homeassistant/components/nibe_heatpump/translations/sv.json index 4e0c9cdd7ca..5406e5b407f 100644 --- a/homeassistant/components/nibe_heatpump/translations/sv.json +++ b/homeassistant/components/nibe_heatpump/translations/sv.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" - }, "error": { "address": "Ogiltig fj\u00e4rr-IP-adress har angetts. Adressen m\u00e5ste vara en IPv4-adress.", "address_in_use": "Den valda lyssningsporten anv\u00e4nds redan p\u00e5 detta system.", @@ -10,16 +7,6 @@ "read": "Fel p\u00e5 l\u00e4sf\u00f6rfr\u00e5gan fr\u00e5n pumpen. Verifiera din \"Fj\u00e4rrl\u00e4sningsport\" eller \"Fj\u00e4rr-IP-adress\".", "unknown": "Ov\u00e4ntat fel", "write": "Fel vid skrivbeg\u00e4ran till pumpen. Verifiera din `Fj\u00e4rrskrivport` eller `Fj\u00e4rr-IP-adress`." - }, - "step": { - "user": { - "data": { - "ip_address": "Fj\u00e4rr IP-adress", - "listening_port": "Lokal lyssningsport", - "remote_read_port": "Port f\u00f6r fj\u00e4rravl\u00e4sning", - "remote_write_port": "Port f\u00f6r fj\u00e4rrskrivning" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/tr.json b/homeassistant/components/nibe_heatpump/translations/tr.json index 3e7c744ca31..05b75d58f18 100644 --- a/homeassistant/components/nibe_heatpump/translations/tr.json +++ b/homeassistant/components/nibe_heatpump/translations/tr.json @@ -1,10 +1,7 @@ { "config": { - "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" - }, "error": { - "address": "Ge\u00e7ersiz uzak IP adresi belirtildi. Adres bir IPV4 adresi olmal\u0131d\u0131r.", + "address": "Ge\u00e7ersiz uzak adres belirtildi. Adres bir IP adresi veya \u00e7\u00f6z\u00fclebilir bir ana bilgisayar ad\u0131 olmal\u0131d\u0131r.", "address_in_use": "Se\u00e7ilen dinleme ba\u011flant\u0131 noktas\u0131 bu sistemde zaten kullan\u0131l\u0131yor.", "model": "Se\u00e7ilen model modbus40'\u0131 desteklemiyor gibi g\u00f6r\u00fcn\u00fcyor", "read": "Pompadan okuma iste\u011finde hata. 'Uzaktan okuma ba\u011flant\u0131 noktas\u0131' veya 'Uzak IP adresinizi' do\u011frulay\u0131n.", @@ -13,15 +10,6 @@ }, "step": { "user": { - "data": { - "ip_address": "Uzak IP adresi", - "listening_port": "Yerel dinleme ba\u011flant\u0131 noktas\u0131", - "remote_read_port": "Uzaktan okuma ba\u011flant\u0131 noktas\u0131", - "remote_write_port": "Uzaktan yazma ba\u011flant\u0131 noktas\u0131" - }, - "data_description": { - "remote_write_port": "NibeGW biriminin yazma isteklerini dinledi\u011fi ba\u011flant\u0131 noktas\u0131." - }, "description": "Entegrasyonu yap\u0131land\u0131rmaya \u00e7al\u0131\u015fmadan \u00f6nce \u015funlar\u0131 do\u011frulay\u0131n:\n - NibeGW \u00fcnitesi bir \u0131s\u0131 pompas\u0131na ba\u011fl\u0131d\u0131r.\n - Is\u0131 pompas\u0131 konfig\u00fcrasyonunda MODBUS40 aksesuar\u0131 etkinle\u015ftirildi.\n - Pompa, eksik MODBUS40 aksesuar\u0131 ile ilgili alarm durumuna ge\u00e7medi." } } diff --git a/homeassistant/components/nibe_heatpump/translations/zh-Hans.json b/homeassistant/components/nibe_heatpump/translations/zh-Hans.json deleted file mode 100644 index 527e3717c4a..00000000000 --- a/homeassistant/components/nibe_heatpump/translations/zh-Hans.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "ip_address": "\u8fdc\u7a0bIP\u5730\u5740", - "listening_port": "\u672c\u5730\u76d1\u542c\u7aef\u53e3", - "remote_read_port": "\u8fdc\u7a0b\u8bfb\u53d6\u7aef\u53e3", - "remote_write_port": "\u8fdc\u7a0b\u5199\u5165\u7aef\u53e3" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json index a2bb8c8f023..f56a9ad6f55 100644 --- a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json +++ b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json @@ -1,23 +1,47 @@ { "config": { - "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, "error": { - "address": "\u6307\u5b9a\u7684\u9060\u7aef IP \u4f4d\u5740\u7121\u6548\u3002\u4f4d\u5740\u5fc5\u9808\u70ba IPV4 \u4f4d\u5740\u3002", + "address": "\u6307\u5b9a\u7684\u9060\u7aef\u4f4d\u5740\u7121\u6548\u3002\u4f4d\u5740\u5fc5\u9808\u70ba IP \u4f4d\u5740\u6216\u53ef\u89e3\u6790\u7684\u4e3b\u6a5f\u540d\u7a31\u3002", "address_in_use": "\u6240\u9078\u64c7\u7684\u76e3\u807d\u901a\u8a0a\u57e0\u5df2\u7d93\u88ab\u7cfb\u7d71\u6240\u4f7f\u7528\u3002", - "model": "\u6240\u9078\u64c7\u7684\u578b\u865f\u4f3c\u4e4e\u4e0d\u652f\u63f4 modbus40", - "read": "\u8b80\u53d6\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u8b80\u53d6\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002", + "model": "\u6240\u9078\u64c7\u7684\u578b\u865f\u4f3c\u4e4e\u4e0d\u652f\u63f4 MODBUS40", + "read": "\u8b80\u53d6\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u8b80\u53d6\u57e0` \u6216 `\u9060\u7aef\u4f4d\u5740`\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "write": "\u5beb\u5165\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u5beb\u5165\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002" + "url": "\u6307\u5b9a\u7684 URL \u4e0d\u662f\u6b63\u78ba\u683c\u5f0f\u6216\u652f\u63f4\u7684 URL", + "write": "\u5beb\u5165\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u5beb\u5165\u57e0` \u6216 `\u9060\u7aef\u4f4d\u5740`\u3002" }, "step": { - "user": { + "modbus": { "data": { - "ip_address": "\u9060\u7aef IP \u4f4d\u5740", + "modbus_unit": "Modbus \u8a2d\u5099\u8b58\u5225", + "modbus_url": "Modbus URL", + "model": "\u71b1\u6cf5\u578b\u865f" + }, + "data_description": { + "modbus_unit": "\u71b1\u6cf5\u8a2d\u5099\u8b58\u5225\uff0c\u901a\u5e38\u53ef\u4ee5\u4fdd\u7559\u70ba 0\u3002", + "modbus_url": "Modbus URL \u70ba\u9023\u7dda\u81f3\u71b1\u6cf5\u6216 MODBUS40 \u8a2d\u5099\u4e4b\u5167\u5bb9\u3001\u61c9\u8a72\u70ba\u4e0b\u5217\u683c\u5f0f\uff1a\n - `tcp://[HOST]:[PORT]` \u7528\u70ba Modbus TCP \u9023\u7dda\n - `serial://[LOCAL DEVICE]` \u7528\u70ba\u672c\u5730\u7aef Modbus RTU \u9023\u7dda\n - `rfc2217://[HOST]:[PORT]` \u7528\u70ba\u9060\u7aef telnet \u985e\u578b Modbus RTU \u9023\u7dda\u3002" + } + }, + "nibegw": { + "data": { + "ip_address": "\u9060\u7aef\u4f4d\u5740", "listening_port": "\u672c\u5730\u76e3\u807d\u901a\u8a0a\u57e0", + "model": "\u71b1\u6cf5\u578b\u865f", "remote_read_port": "\u9060\u7aef\u8b80\u53d6\u57e0", "remote_write_port": "\u9060\u7aef\u5beb\u5165\u57e0" + }, + "data_description": { + "ip_address": "NibeGW \u8a2d\u5099\u4f4d\u5740\u3002\u88dd\u7f6e\u61c9\u8a72\u5df2\u7d93\u8a2d\u5b9a\u70ba\u975c\u614b\u4f4d\u5740\uff0c", + "listening_port": "\u7cfb\u7d71\u672c\u5730\u901a\u8a0a\u57e0\u3001\u4f9b NibeGW \u8a2d\u5099\u8a2d\u5b9a\u50b3\u9001\u8cc7\u6599\u3002", + "remote_read_port": "NibeGW \u8a2d\u5099\u76e3\u807d\u8b80\u53d6\u8acb\u6c42\u901a\u8a0a\u57e0\u3002", + "remote_write_port": "NibeGW \u8a2d\u5099\u76e3\u807d\u5beb\u5165\u8acb\u6c42\u901a\u8a0a\u57e0\u3002" + }, + "description": "\u65bc\u5617\u8a66\u8a2d\u5b9a\u6574\u5408\u524d\u3001\u8acb\u78ba\u8a8d\uff1a\n - NibeGW \u8a2d\u5099\u5df2\u7d93\u9023\u7dda\u81f3\u71b1\u6cf5\u3002\n - \u5df2\u7d93\u65bc\u71b1\u6cf5\u8a2d\u5b9a\u4e2d\u555f\u7528 MODBUS40 \u914d\u4ef6\u3002\n - \u6cf5\u4e26\u6c92\u6709\u51fa\u73fe\u7f3a\u5c11 MODBUS40 \u914d\u4ef6\u4e4b\u8b66\u544a\u3002" + }, + "user": { + "description": "\u9078\u64c7\u6cf5\u9023\u7dda\u6a21\u5f0f\u3002\u901a\u5e38\u3001F \u7cfb\u5217\u6cf5\u9700\u8981 Nibe GW \u81ea\u8a02\u914d\u4ef6\u3001\u800c S \u7cfb\u5217\u6cf5\u70ba\u5167\u5efa Modbus\u3002", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" } } } diff --git a/homeassistant/components/nightscout/translations/sk.json b/homeassistant/components/nightscout/translations/sk.json index ff853127803..de3386fca50 100644 --- a/homeassistant/components/nightscout/translations/sk.json +++ b/homeassistant/components/nightscout/translations/sk.json @@ -1,13 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" - } + "api_key": "API k\u013e\u00fa\u010d", + "url": "URL" + }, + "title": "Zadajte inform\u00e1cie o serveri Nightscout." } } } diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index 76280ab159e..bdc79c34d92 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -62,12 +62,12 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti """Initialize.""" super().__init__(coordinator) - self._region: str = region - self._warning_index: int = slot_id - 1 + self._region = region + self._warning_index = slot_id - 1 - self._attr_name: str = f"Warning: {region_name} {slot_id}" - self._attr_unique_id: str = f"{region}-{slot_id}" - self._attr_device_class: str = BinarySensorDeviceClass.SAFETY + self._attr_name = f"Warning: {region_name} {slot_id}" + self._attr_unique_id = f"{region}-{slot_id}" + self._attr_device_class = BinarySensorDeviceClass.SAFETY @property def is_on(self) -> bool: diff --git a/homeassistant/components/nina/translations/de.json b/homeassistant/components/nina/translations/de.json index 4e6b881b051..f1aad460975 100644 --- a/homeassistant/components/nina/translations/de.json +++ b/homeassistant/components/nina/translations/de.json @@ -17,7 +17,7 @@ "_m_to_q": "Stadt/Landkreis (M-Q)", "_r_to_u": "Stadt/Landkreis (R-U)", "_v_to_z": "Stadt/Landkreis (V-Z)", - "corona_filter": "Corona-Warnungen entfernen", + "corona_filter": "Corona Warnungen entfernen", "slots": "Maximale Warnungen pro Stadt/Landkreis" }, "title": "Stadt/Landkreis ausw\u00e4hlen" diff --git a/homeassistant/components/nina/translations/sk.json b/homeassistant/components/nina/translations/sk.json new file mode 100644 index 00000000000..20fc41d8c6f --- /dev/null +++ b/homeassistant/components/nina/translations/sk.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_selection": "Vyberte aspo\u0148 jedno mesto/okres", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Mesto/okres (A-D)", + "_e_to_h": "Mesto/okres (E-H)", + "_i_to_l": "Mesto/okres (I-L)", + "_m_to_q": "Mesto/okres (M-Q)", + "_r_to_u": "Mesto/okres (R-U)", + "_v_to_z": "Mesto/okres (V-Z)", + "corona_filter": "Odstr\u00e1\u0148te v\u00fdstrahy Corona", + "slots": "Maxim\u00e1lny po\u010det varovan\u00ed na mesto/okres" + }, + "title": "Vyberte mesto/okres" + } + } + }, + "options": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_selection": "Vyberte aspo\u0148 jedno mesto/okres", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Mesto/okres (A-D)", + "_e_to_h": "Mesto/okres (E-H)", + "_i_to_l": "Mesto/okres (I-L)", + "_m_to_q": "Mesto/okres (M-Q)", + "_r_to_u": "Mesto/okres (R-U)", + "_v_to_z": "Mesto/okres (V-Z)", + "corona_filter": "Odstr\u00e1\u0148te v\u00fdstrahy Corona", + "slots": "Maxim\u00e1lny po\u010det varovan\u00ed na mesto/okres" + }, + "title": "Mo\u017enosti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 75757ae4a25..a32ba2e6329 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -500,7 +500,7 @@ class LeafDataStore: class LeafEntity(Entity): """Base class for Nissan Leaf entity.""" - def __init__(self, car: Leaf) -> None: + def __init__(self, car: LeafDataStore) -> None: """Store LeafDataStore upon init.""" self.car = car diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 7b8173cd31b..40880714c2a 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -3,8 +3,6 @@ from __future__ import annotations import logging -from pycarwings2.pycarwings2 import Leaf - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -13,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import LeafEntity +from . import LeafDataStore, LeafEntity from .const import DATA_CHARGING, DATA_LEAF, DATA_PLUGGED_IN _LOGGER = logging.getLogger(__name__) @@ -43,7 +41,7 @@ class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.PLUG - def __init__(self, car: Leaf) -> None: + def __init__(self, car: LeafDataStore) -> None: """Set up plug status sensor.""" super().__init__(car) self._attr_unique_id = f"{self.car.leaf.vin.lower()}_plugstatus" @@ -69,7 +67,7 @@ class LeafChargingSensor(LeafEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING - def __init__(self, car: Leaf) -> None: + def __init__(self, car: LeafDataStore) -> None: """Set up charging status sensor.""" super().__init__(car) self._attr_unique_id = f"{self.car.leaf.vin.lower()}_chargingstatus" diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index c92ed2300a4..b0da5757db9 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from pycarwings2.pycarwings2 import Leaf from voluptuous.validators import Number from homeassistant.components.sensor import SensorDeviceClass, SensorEntity @@ -15,7 +14,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import LeafEntity +from . import LeafDataStore, LeafEntity from .const import ( DATA_BATTERY, DATA_CHARGING, @@ -26,8 +25,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -ICON_RANGE = "mdi:speedometer" - def setup_platform( hass: HomeAssistant, @@ -52,7 +49,10 @@ def setup_platform( class LeafBatterySensor(LeafEntity, SensorEntity): """Nissan Leaf Battery Sensor.""" - def __init__(self, car: Leaf) -> None: + _attr_device_class = SensorDeviceClass.BATTERY + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__(self, car: LeafDataStore) -> None: """Set up battery sensor.""" super().__init__(car) self._attr_unique_id = f"{self.car.leaf.vin.lower()}_soc" @@ -62,11 +62,6 @@ class LeafBatterySensor(LeafEntity, SensorEntity): """Sensor Name.""" return f"{self.car.leaf.nickname} Charge" - @property - def device_class(self) -> str: - """Return the device class of the sensor.""" - return SensorDeviceClass.BATTERY - @property def native_value(self) -> Number | None: """Battery state percentage.""" @@ -74,11 +69,6 @@ class LeafBatterySensor(LeafEntity, SensorEntity): return None return round(self.car.data[DATA_BATTERY]) - @property - def native_unit_of_measurement(self) -> str: - """Battery state measured in percentage.""" - return PERCENTAGE - @property def icon(self) -> str: """Battery state icon handling.""" @@ -89,7 +79,9 @@ class LeafBatterySensor(LeafEntity, SensorEntity): class LeafRangeSensor(LeafEntity, SensorEntity): """Nissan Leaf Range Sensor.""" - def __init__(self, car: Leaf, ac_on: bool) -> None: + _attr_icon = "mdi:speedometer" + + def __init__(self, car: LeafDataStore, ac_on: bool) -> None: """Set up range sensor. Store if AC on.""" self._ac_on = ac_on super().__init__(car) @@ -115,6 +107,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity): @property def native_value(self) -> float | None: """Battery range in miles or kms.""" + ret: float | None if self._ac_on: ret = self.car.data[DATA_RANGE_AC] else: @@ -134,8 +127,3 @@ class LeafRangeSensor(LeafEntity, SensorEntity): if self.car.hass.config.units is US_CUSTOMARY_SYSTEM or self.car.force_miles: return LENGTH_MILES return LENGTH_KILOMETERS - - @property - def icon(self) -> str: - """Nice icon for range.""" - return ICON_RANGE diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 0d655622517..97f02a9e0be 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -4,14 +4,12 @@ from __future__ import annotations import logging from typing import Any -from pycarwings2.pycarwings2 import Leaf - from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import LeafEntity +from . import LeafDataStore, LeafEntity from .const import DATA_CLIMATE, DATA_LEAF _LOGGER = logging.getLogger(__name__) @@ -38,7 +36,7 @@ def setup_platform( class LeafClimateSwitch(LeafEntity, SwitchEntity): """Nissan Leaf Climate Control switch.""" - def __init__(self, car: Leaf) -> None: + def __init__(self, car: LeafDataStore) -> None: """Set up climate control switch.""" super().__init__(car) self._attr_unique_id = f"{self.car.leaf.vin.lower()}_climatecontrol" diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 9203288f03a..bada45256a8 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -4,8 +4,7 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 6e7a9cbee53..a3e7ed50bd3 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "netmap==0.7.0.2", "getmac==0.8.2", - "mac-vendor-lookup==0.1.11" + "mac-vendor-lookup==0.1.12" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/components/nmap_tracker/translations/cs.json b/homeassistant/components/nmap_tracker/translations/cs.json index 95f0bfc3ae8..093da4eff30 100644 --- a/homeassistant/components/nmap_tracker/translations/cs.json +++ b/homeassistant/components/nmap_tracker/translations/cs.json @@ -2,12 +2,20 @@ "config": { "abort": { "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "data": { + "hosts": "S\u00ed\u0165ov\u00e9 adresy (odd\u011blen\u00e9 \u010d\u00e1rkami), kter\u00e9 se maj\u00ed skenovat" + } + } } }, "options": { "step": { "init": { "data": { + "hosts": "S\u00ed\u0165ov\u00e9 adresy (odd\u011blen\u00e9 \u010d\u00e1rkami), kter\u00e9 se maj\u00ed skenovat", "interval_seconds": "Interval skenov\u00e1n\u00ed" } } diff --git a/homeassistant/components/nmap_tracker/translations/de.json b/homeassistant/components/nmap_tracker/translations/de.json index cfab5b828fd..074bdac0e4a 100644 --- a/homeassistant/components/nmap_tracker/translations/de.json +++ b/homeassistant/components/nmap_tracker/translations/de.json @@ -29,7 +29,7 @@ "exclude": "Netzwerkadressen (kommagetrennt), die von der \u00dcberpr\u00fcfung ausgeschlossen werden sollen", "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)", "hosts": "Netzwerkadressen (kommagetrennt) zum Scannen", - "interval_seconds": "Scanintervall", + "interval_seconds": "Scan Intervall", "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap" }, "description": "Konfiguriere die Hosts, die von Nmap gescannt werden sollen. Netzwerkadresse und Ausschl\u00fcsse k\u00f6nnen IP-Adressen (192.168.1.1), IP-Netzwerke (192.168.0.0/24) oder IP-Bereiche (192.168.1.0-32) sein." diff --git a/homeassistant/components/nmap_tracker/translations/sk.json b/homeassistant/components/nmap_tracker/translations/sk.json new file mode 100644 index 00000000000..e2c968a9e58 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "invalid_hosts": "Neplatn\u00ed hostitelia" + }, + "step": { + "user": { + "data": { + "exclude": "Sie\u0165ov\u00e9 adresy (oddelen\u00e9 \u010diarkou), ktor\u00e9 sa maj\u00fa vyl\u00fa\u010di\u0165 z kontroly", + "home_interval": "Minim\u00e1lny po\u010det min\u00fat medzi kontrolami akt\u00edvnych zariaden\u00ed (\u0161etrenie bat\u00e9rie)", + "hosts": "Sie\u0165ov\u00e9 adresy (oddelen\u00e9 \u010diarkami), ktor\u00e9 sa maj\u00fa skenova\u0165" + } + } + } + }, + "options": { + "error": { + "invalid_hosts": "Neplatn\u00ed hostitelia" + }, + "step": { + "init": { + "data": { + "exclude": "Sie\u0165ov\u00e9 adresy (oddelen\u00e9 \u010diarkou), ktor\u00e9 sa maj\u00fa vyl\u00fa\u010di\u0165 z kontroly", + "home_interval": "Minim\u00e1lny po\u010det min\u00fat medzi kontrolami akt\u00edvnych zariaden\u00ed (\u0161etrenie bat\u00e9rie)", + "hosts": "Sie\u0165ov\u00e9 adresy (oddelen\u00e9 \u010diarkami), ktor\u00e9 sa maj\u00fa skenova\u0165", + "interval_seconds": "Interval skenovania" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/it.json b/homeassistant/components/nobo_hub/translations/it.json index 2b8100c49e2..28bb8f94f34 100644 --- a/homeassistant/components/nobo_hub/translations/it.json +++ b/homeassistant/components/nobo_hub/translations/it.json @@ -25,7 +25,7 @@ }, "user": { "data": { - "device": "Hub scoperti" + "device": "Rilevati hub" }, "description": "Seleziona Nob\u00f8 Ecohub da configurare." } diff --git a/homeassistant/components/nobo_hub/translations/sk.json b/homeassistant/components/nobo_hub/translations/sk.json new file mode 100644 index 00000000000..f862703bd72 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165 - skontrolujte s\u00e9riov\u00e9 \u010d\u00edslo", + "invalid_ip": "Neplatn\u00e1 IP adresa", + "invalid_serial": "Neplatn\u00e9 s\u00e9riov\u00e9 \u010d\u00edslo", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP adresa", + "serial": "S\u00e9riov\u00e9 \u010d\u00edslo (12 \u010d\u00edslic)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index c3bb02896e0..35c1bbe65bf 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -41,7 +41,7 @@ class LegacyNotifyPlatform(Protocol): hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = ..., - ) -> BaseNotificationService: + ) -> BaseNotificationService | None: """Set up notification service.""" def get_service( @@ -49,7 +49,7 @@ class LegacyNotifyPlatform(Protocol): hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = ..., - ) -> BaseNotificationService: + ) -> BaseNotificationService | None: """Set up notification service.""" @@ -82,7 +82,7 @@ def async_setup_legacy( full_name = f"{DOMAIN}.{integration_name}" LOGGER.info("Setting up %s", full_name) with async_start_setup(hass, [full_name]): - notify_service = None + notify_service: BaseNotificationService | None = None try: if hasattr(platform, "async_get_service"): notify_service = await platform.async_get_service( @@ -282,7 +282,7 @@ class BaseNotificationService: if hasattr(self, "targets"): stale_targets = set(self.registered_targets) - for name, target in self.targets.items(): # type: ignore[attr-defined] + for name, target in self.targets.items(): target_name = slugify(f"{self._target_service_name_prefix}_{name}") if target_name in stale_targets: stale_targets.remove(target_name) diff --git a/homeassistant/components/notion/translations/bg.json b/homeassistant/components/notion/translations/bg.json index 1df2ad13a33..ddf339824c9 100644 --- a/homeassistant/components/notion/translations/bg.json +++ b/homeassistant/components/notion/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/notion/translations/sk.json b/homeassistant/components/notion/translations/sk.json index 71a7aea5018..66a5aae1f52 100644 --- a/homeassistant/components/notion/translations/sk.json +++ b/homeassistant/components/notion/translations/sk.json @@ -1,10 +1,28 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Znova zadajte heslo pre {username}.", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Vypl\u0148te svoje \u00fadaje" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 694089b1396..97e67f1f0a4 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aio_geojson_nsw_rfs_incidents==0.4"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["aio_geojson_nsw_rfs_incidents"] + "loggers": ["aio_geojson_nsw_rfs_incidents"], + "integration_type": "service" } diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index c731e3472d6..0c053a8c67f 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -1,7 +1,5 @@ """Support for NuHeat thermostats.""" -from datetime import datetime import logging -import time from typing import Any from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD @@ -27,16 +25,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DOMAIN, - MANUFACTURER, - NUHEAT_API_STATE_SHIFT_DELAY, - NUHEAT_DATETIME_FORMAT, - NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME, - NUHEAT_KEY_SCHEDULE_MODE, - NUHEAT_KEY_SET_POINT_TEMP, - TEMP_HOLD_TIME_SEC, -) +from .const import DOMAIN, MANUFACTURER, NUHEAT_API_STATE_SHIFT_DELAY _LOGGER = logging.getLogger(__name__) @@ -225,22 +214,9 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): target_schedule_mode, ) - target_temperature = max( - min(self._thermostat.max_temperature, target_temperature), - self._thermostat.min_temperature, + self._thermostat.set_target_temperature( + target_temperature, target_schedule_mode ) - - request = { - NUHEAT_KEY_SET_POINT_TEMP: target_temperature, - NUHEAT_KEY_SCHEDULE_MODE: target_schedule_mode, - } - - if target_schedule_mode == SCHEDULE_TEMPORARY_HOLD: - request[NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME] = datetime.fromtimestamp( - time.time() + TEMP_HOLD_TIME_SEC - ).strftime(NUHEAT_DATETIME_FORMAT) - - self._thermostat.set_data(request) self._schedule_mode = target_schedule_mode self._target_temperature = target_temperature self._schedule_update() diff --git a/homeassistant/components/nuheat/const.py b/homeassistant/components/nuheat/const.py index 619d4a11e2a..ea43c33d9b0 100644 --- a/homeassistant/components/nuheat/const.py +++ b/homeassistant/components/nuheat/const.py @@ -10,10 +10,3 @@ CONF_SERIAL_NUMBER = "serial_number" MANUFACTURER = "NuHeat" NUHEAT_API_STATE_SHIFT_DELAY = 2 - -TEMP_HOLD_TIME_SEC = 43200 - -NUHEAT_KEY_SET_POINT_TEMP = "SetPointTemp" -NUHEAT_KEY_SCHEDULE_MODE = "ScheduleMode" -NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME = "HoldSetPointDateTime" -NUHEAT_DATETIME_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index aea63a692a5..90d18b87af2 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -2,8 +2,8 @@ "domain": "nuheat", "name": "NuHeat", "documentation": "https://www.home-assistant.io/integrations/nuheat", - "requirements": ["nuheat==0.3.0"], - "codeowners": [], + "requirements": ["nuheat==1.0.0"], + "codeowners": ["@tstabrawa"], "config_flow": true, "dhcp": [ { diff --git a/homeassistant/components/nuheat/translations/bg.json b/homeassistant/components/nuheat/translations/bg.json index 03ace4428b1..47f6792c3b0 100644 --- a/homeassistant/components/nuheat/translations/bg.json +++ b/homeassistant/components/nuheat/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" } diff --git a/homeassistant/components/nuheat/translations/sk.json b/homeassistant/components/nuheat/translations/sk.json index 5ada995aa6e..55ec6c3046c 100644 --- a/homeassistant/components/nuheat/translations/sk.json +++ b/homeassistant/components/nuheat/translations/sk.json @@ -1,7 +1,23 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_thermostat": "S\u00e9riov\u00e9 \u010d\u00edslo termostatu je neplatn\u00e9.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "serial_number": "S\u00e9riov\u00e9 \u010d\u00edslo termostatu.", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripojte sa k NuHeat" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 85144d9bb77..310197d55d8 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -8,13 +8,13 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN -from .helpers import parse_id +from .helpers import CannotConnect, InvalidAuth, parse_id _LOGGER = logging.getLogger(__name__) @@ -153,11 +153,3 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=data_schema, errors=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/nuki/helpers.py b/homeassistant/components/nuki/helpers.py index 3deedf9d8db..45b7420754a 100644 --- a/homeassistant/components/nuki/helpers.py +++ b/homeassistant/components/nuki/helpers.py @@ -1,6 +1,15 @@ """nuki integration helpers.""" +from homeassistant import exceptions def parse_id(hardware_id): """Parse Nuki ID.""" return hex(hardware_id).split("x")[-1].upper() + + +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/nuki/lock.py b/homeassistant/components/nuki/lock.py index 8b6c843f48a..4b89b0d3535 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -6,6 +6,7 @@ from typing import Any from pynuki import NukiLock, NukiOpener from pynuki.constants import MODE_OPENER_CONTINUOUS +from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.lock import LockEntity, LockEntityFeature @@ -26,6 +27,7 @@ from .const import ( DOMAIN as NUKI_DOMAIN, ERROR_STATES, ) +from .helpers import CannotConnect async def async_setup_entry( @@ -114,15 +116,24 @@ class NukiLockEntity(NukiDeviceEntity): def lock(self, **kwargs: Any) -> None: """Lock the device.""" - self._nuki_device.lock() + try: + self._nuki_device.lock() + except RequestException as err: + raise CannotConnect from err def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" - self._nuki_device.unlock() + try: + self._nuki_device.unlock() + except RequestException as err: + raise CannotConnect from err def open(self, **kwargs: Any) -> None: """Open the door latch.""" - self._nuki_device.unlatch() + try: + self._nuki_device.unlatch() + except RequestException as err: + raise CannotConnect from err def lock_n_go(self, unlatch: bool) -> None: """Lock and go. @@ -130,7 +141,10 @@ class NukiLockEntity(NukiDeviceEntity): This will first unlock the door, then wait for 20 seconds (or another amount of time depending on the lock settings) and relock. """ - self._nuki_device.lock_n_go(unlatch) + try: + self._nuki_device.lock_n_go(unlatch) + except RequestException as err: + raise CannotConnect from err class NukiOpenerEntity(NukiDeviceEntity): @@ -148,15 +162,24 @@ class NukiOpenerEntity(NukiDeviceEntity): def lock(self, **kwargs: Any) -> None: """Disable ring-to-open.""" - self._nuki_device.deactivate_rto() + try: + self._nuki_device.deactivate_rto() + except RequestException as err: + raise CannotConnect from err def unlock(self, **kwargs: Any) -> None: """Enable ring-to-open.""" - self._nuki_device.activate_rto() + try: + self._nuki_device.activate_rto() + except RequestException as err: + raise CannotConnect from err def open(self, **kwargs: Any) -> None: """Buzz open the door.""" - self._nuki_device.electric_strike_actuation() + try: + self._nuki_device.electric_strike_actuation() + except RequestException as err: + raise CannotConnect from err def lock_n_go(self, unlatch: bool) -> None: """Stub service.""" @@ -168,7 +191,10 @@ class NukiOpenerEntity(NukiDeviceEntity): rings the bell. This is similar to ring-to-open, except that it does not automatically deactivate """ - if enable: - self._nuki_device.activate_continuous_mode() - else: - self._nuki_device.deactivate_continuous_mode() + try: + if enable: + self._nuki_device.activate_continuous_mode() + else: + self._nuki_device.deactivate_continuous_mode() + except RequestException as err: + raise CannotConnect from err diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 8a9b7c506b4..de8edd6af40 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,7 +2,7 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.5.2"], + "requirements": ["pynuki==1.6.0"], "codeowners": ["@pschmitt", "@pvizeli", "@pree"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json index 1a6aff3fe4c..d97503db4ad 100644 --- a/homeassistant/components/nuki/translations/bg.json +++ b/homeassistant/components/nuki/translations/bg.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/nuki/translations/sk.json b/homeassistant/components/nuki/translations/sk.json index 16e76236805..bd03a6ec700 100644 --- a/homeassistant/components/nuki/translations/sk.json +++ b/homeassistant/components/nuki/translations/sk.json @@ -4,16 +4,20 @@ "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "reauth_confirm": { "data": { "token": "Pr\u00edstupov\u00fd token" - } + }, + "title": "Znova overi\u0165 integr\u00e1ciu" }, "user": { "data": { + "host": "Hostite\u013e", "port": "Port", "token": "Pr\u00edstupov\u00fd token" } diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 0012a4b77ff..dfd14f5257b 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -55,8 +55,257 @@ _LOGGER = logging.getLogger(__name__) class NumberDeviceClass(StrEnum): """Device class for numbers.""" - # temperature (C/F) + # NumberDeviceClass should be aligned with SensorDeviceClass + + APPARENT_POWER = "apparent_power" + """Apparent power. + + Unit of measurement: `VA` + """ + + AQI = "aqi" + """Air Quality Index. + + Unit of measurement: `None` + """ + + BATTERY = "battery" + """Percentage of battery that is left. + + Unit of measurement: `%` + """ + + CO = "carbon_monoxide" + """Carbon Monoxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CO2 = "carbon_dioxide" + """Carbon Dioxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CURRENT = "current" + """Current. + + Unit of measurement: `A` + """ + + DISTANCE = "distance" + """Generic distance. + + Unit of measurement: `LENGTH_*` units + - SI /metric: `mm`, `cm`, `m`, `km` + - USCS / imperial: `in`, `ft`, `yd`, `mi` + """ + + ENERGY = "energy" + """Energy. + + Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` + """ + + FREQUENCY = "frequency" + """Frequency. + + Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` + """ + + GAS = "gas" + """Gas. + + Unit of measurement: `m³`, `ft³` + """ + + HUMIDITY = "humidity" + """Relative humidity. + + Unit of measurement: `%` + """ + + ILLUMINANCE = "illuminance" + """Illuminance. + + Unit of measurement: `lx`, `lm` + """ + + MOISTURE = "moisture" + """Moisture. + + Unit of measurement: `%` + """ + + MONETARY = "monetary" + """Amount of money. + + Unit of measurement: ISO4217 currency code + + See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes + """ + + NITROGEN_DIOXIDE = "nitrogen_dioxide" + """Amount of NO2. + + Unit of measurement: `µg/m³` + """ + + NITROGEN_MONOXIDE = "nitrogen_monoxide" + """Amount of NO. + + Unit of measurement: `µg/m³` + """ + + NITROUS_OXIDE = "nitrous_oxide" + """Amount of N2O. + + Unit of measurement: `µg/m³` + """ + + OZONE = "ozone" + """Amount of O3. + + Unit of measurement: `µg/m³` + """ + + PM1 = "pm1" + """Particulate matter <= 0.1 μm. + + Unit of measurement: `µg/m³` + """ + + PM10 = "pm10" + """Particulate matter <= 10 μm. + + Unit of measurement: `µg/m³` + """ + + PM25 = "pm25" + """Particulate matter <= 2.5 μm. + + Unit of measurement: `µg/m³` + """ + + POWER_FACTOR = "power_factor" + """Power factor. + + Unit of measurement: `%` + """ + + POWER = "power" + """Power. + + Unit of measurement: `W`, `kW` + """ + + PRECIPITATION = "precipitation" + """Precipitation. + + Unit of measurement: + - SI / metric: `mm` + - USCS / imperial: `in` + """ + + PRECIPITATION_INTENSITY = "precipitation_intensity" + """Precipitation intensity. + + Unit of measurement: UnitOfVolumetricFlux + - SI /metric: `mm/d`, `mm/h` + - USCS / imperial: `in/d`, `in/h` + """ + + PRESSURE = "pressure" + """Pressure. + + Unit of measurement: + - `mbar`, `cbar`, `bar` + - `Pa`, `hPa`, `kPa` + - `inHg` + - `psi` + """ + + REACTIVE_POWER = "reactive_power" + """Reactive power. + + Unit of measurement: `var` + """ + + SIGNAL_STRENGTH = "signal_strength" + """Signal strength. + + Unit of measurement: `dB`, `dBm` + """ + + SPEED = "speed" + """Generic speed. + + Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` + - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` + - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` + - Nautical: `kn` + """ + + SULPHUR_DIOXIDE = "sulphur_dioxide" + """Amount of SO2. + + Unit of measurement: `µg/m³` + """ + TEMPERATURE = "temperature" + """Temperature. + + Unit of measurement: `°C`, `°F` + """ + + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" + """Amount of VOC. + + Unit of measurement: `µg/m³` + """ + + VOLTAGE = "voltage" + """Voltage. + + Unit of measurement: `V` + """ + + VOLUME = "volume" + """Generic volume. + + Unit of measurement: `VOLUME_*` units + - SI / metric: `mL`, `L`, `m³` + - USCS / imperial: `fl. oz.`, `ft³`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WATER = "water" + """Water. + + Unit of measurement: + - SI / metric: `m³`, `L` + - USCS / imperial: `ft³`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WEIGHT = "weight" + """Generic weight, represents a measurement of an object's mass. + + Weight is used instead of mass to fit with every day language. + + Unit of measurement: `MASS_*` units + - SI / metric: `µg`, `mg`, `g`, `kg` + - USCS / imperial: `oz`, `lb` + """ + + WIND_SPEED = "wind_speed" + """Wind speed. + + Unit of measurement: `SPEED_*` units + - SI /metric: `m/s`, `km/h` + - USCS / imperial: `ft/s`, `mph` + - Nautical: `kn` + """ DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass)) diff --git a/homeassistant/components/number/translations/sk.json b/homeassistant/components/number/translations/sk.json new file mode 100644 index 00000000000..641f7658bb2 --- /dev/null +++ b/homeassistant/components/number/translations/sk.json @@ -0,0 +1,3 @@ +{ + "title": "\u010c\u00edslo" +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/sk.json b/homeassistant/components/nut/translations/sk.json index d00d818716f..974b12f60d5 100644 --- a/homeassistant/components/nut/translations/sk.json +++ b/homeassistant/components/nut/translations/sk.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "host": "Hostite\u013e", "password": "Heslo", "port": "Port", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" diff --git a/homeassistant/components/nws/translations/sk.json b/homeassistant/components/nws/translations/sk.json index abb3969f6b4..8f8b360f638 100644 --- a/homeassistant/components/nws/translations/sk.json +++ b/homeassistant/components/nws/translations/sk.json @@ -1,12 +1,20 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" - } + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "station": "K\u00f3d stanice METAR" + }, + "title": "Pripojenie k N\u00e1rodnej meteorologickej slu\u017ebe" } } } diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 7963c1161a9..341f35353ef 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -14,12 +14,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, - LENGTH_METERS, - PRESSURE_PA, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo @@ -153,7 +151,7 @@ class NWSWeather(WeatherEntity): @property def native_temperature_unit(self): """Return the current temperature unit.""" - return TEMP_CELSIUS + return UnitOfTemperature.CELSIUS @property def native_pressure(self): @@ -165,7 +163,7 @@ class NWSWeather(WeatherEntity): @property def native_pressure_unit(self): """Return the current pressure unit.""" - return PRESSURE_PA + return UnitOfPressure.PA @property def humidity(self): @@ -184,7 +182,7 @@ class NWSWeather(WeatherEntity): @property def native_wind_speed_unit(self): """Return the current windspeed.""" - return SPEED_KILOMETERS_PER_HOUR + return UnitOfSpeed.KILOMETERS_PER_HOUR @property def wind_bearing(self): @@ -216,7 +214,7 @@ class NWSWeather(WeatherEntity): @property def native_visibility_unit(self): """Return visibility unit.""" - return LENGTH_METERS + return UnitOfLength.METERS @property def forecast(self): @@ -234,7 +232,7 @@ class NWSWeather(WeatherEntity): if (temp := forecast_entry.get("temperature")) is not None: data[ATTR_FORECAST_NATIVE_TEMP] = TemperatureConverter.convert( - temp, TEMP_FAHRENHEIT, TEMP_CELSIUS + temp, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS ) else: data[ATTR_FORECAST_NATIVE_TEMP] = None @@ -254,7 +252,9 @@ class NWSWeather(WeatherEntity): wind_speed = forecast_entry.get("windSpeedAvg") if wind_speed is not None: data[ATTR_FORECAST_NATIVE_WIND_SPEED] = SpeedConverter.convert( - wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR + wind_speed, + UnitOfSpeed.MILES_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, ) else: data[ATTR_FORECAST_NATIVE_WIND_SPEED] = None diff --git a/homeassistant/components/nzbget/const.py b/homeassistant/components/nzbget/const.py index 673f2531a53..928487738eb 100644 --- a/homeassistant/components/nzbget/const.py +++ b/homeassistant/components/nzbget/const.py @@ -5,7 +5,7 @@ DOMAIN = "nzbget" ATTR_SPEED = "speed" # Data -DATA_COORDINATOR = "corrdinator" +DATA_COORDINATOR = "coordinator" DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" # Defaults diff --git a/homeassistant/components/nzbget/translations/sk.json b/homeassistant/components/nzbget/translations/sk.json index 39d2e182c40..eaaa5cb6d0a 100644 --- a/homeassistant/components/nzbget/translations/sk.json +++ b/homeassistant/components/nzbget/translations/sk.json @@ -1,10 +1,32 @@ { "config": { + "abort": { + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", "step": { "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov", - "port": "Port" + "password": "Heslo", + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + }, + "title": "Pripojte sa k NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frekvencia aktualiz\u00e1cie (sekundy)" } } } diff --git a/homeassistant/components/octoprint/translations/bg.json b/homeassistant/components/octoprint/translations/bg.json index 0635640be7d..e5c0f273c28 100644 --- a/homeassistant/components/octoprint/translations/bg.json +++ b/homeassistant/components/octoprint/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/octoprint/translations/sk.json b/homeassistant/components/octoprint/translations/sk.json new file mode 100644 index 00000000000..4cec5809e7e --- /dev/null +++ b/homeassistant/components/octoprint/translations/sk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "auth_failed": "Nepodarilo sa na\u010d\u00edta\u0165 k\u013e\u00fa\u010d API aplik\u00e1cie", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "Tla\u010diare\u0148 OctoPrint: {host}", + "step": { + "reauth_confirm": { + "data": { + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "host": "Hostite\u013e", + "path": "Cesta aplik\u00e1cie", + "port": "\u010c\u00edslo portu", + "ssl": "Pou\u017eite SSL", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/sk.json b/homeassistant/components/omnilogic/translations/sk.json index 5ada995aa6e..2e4ae17997a 100644 --- a/homeassistant/components/omnilogic/translations/sk.json +++ b/homeassistant/components/omnilogic/translations/sk.json @@ -1,7 +1,29 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ph_offset": "Offset pH (pozit\u00edvny alebo negat\u00edvny)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/onboarding/translations/sk.json b/homeassistant/components/onboarding/translations/sk.json new file mode 100644 index 00000000000..44f2251e344 --- /dev/null +++ b/homeassistant/components/onboarding/translations/sk.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Sp\u00e1l\u0148a", + "kitchen": "Kuchy\u0148a", + "living_room": "Ob\u00fdva\u010dka" + } +} \ No newline at end of file diff --git a/homeassistant/components/oncue/translations/sk.json b/homeassistant/components/oncue/translations/sk.json index 82a91905e7c..5d12e6d8fe7 100644 --- a/homeassistant/components/oncue/translations/sk.json +++ b/homeassistant/components/oncue/translations/sk.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 85a728ee04a..b4c6e02879e 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -50,7 +50,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="ph", name="pH", - native_unit_of_measurement=None, icon="mdi:pool", device_class=None, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/ondilo_ico/translations/sk.json b/homeassistant/components/ondilo_ico/translations/sk.json index c19b1a0b70c..0b766936f38 100644 --- a/homeassistant/components/ondilo_ico/translations/sk.json +++ b/homeassistant/components/ondilo_ico/translations/sk.json @@ -1,7 +1,16 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie." + }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 87adc23ae18..a40b19f2055 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -1,6 +1,7 @@ { "domain": "onewire", "name": "1-Wire", + "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/onewire", "config_flow": true, "requirements": ["pyownet==0.10.0.post1"], diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index feab77f3ec7..81430fd816b 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -26,7 +26,7 @@ "precision": "Sensorgenauigkeit" }, "description": "Sensorgenauigkeit f\u00fcr {sensor_id} ausw\u00e4hlen", - "title": "OneWire-Sensorpr\u00e4zision" + "title": "OneWire Sensorpr\u00e4zision" }, "device_selection": { "data": { @@ -34,7 +34,7 @@ "device_selection": "Zu konfigurierende Ger\u00e4te ausw\u00e4hlen" }, "description": "W\u00e4hle die zu verarbeitenden Konfigurationsschritte aus", - "title": "OneWire-Ger\u00e4teoptionen" + "title": "OneWire Ger\u00e4teoptionen" } } } diff --git a/homeassistant/components/onewire/translations/sk.json b/homeassistant/components/onewire/translations/sk.json index 8b2a2f7343b..636af7a47d1 100644 --- a/homeassistant/components/onewire/translations/sk.json +++ b/homeassistant/components/onewire/translations/sk.json @@ -8,8 +8,25 @@ }, "step": { "user": { + "data": { + "host": "Hostite\u013e", + "port": "Port" + }, "title": "Nastavenie 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Vyberte zariadenia, ktor\u00e9 chcete nakonfigurova\u0165" + }, + "step": { + "configure_device": { + "data": { + "precision": "Presnos\u0165 sn\u00edma\u010da" + }, + "description": "Vyberte presnos\u0165 sn\u00edma\u010da pre {sensor_id}" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index c1d242c840c..d9de2730659 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -252,27 +252,25 @@ class OnkyoDevice(MediaPlayerEntity): ): """Initialize the Onkyo Receiver.""" self._receiver = receiver - self._muted = False - self._volume = 0 - self._pwstate = MediaPlayerState.OFF + self._attr_is_volume_muted = False + self._attr_volume_level = 0 + self._attr_state = MediaPlayerState.OFF if name: # not discovered - self._name = name - self._unique_id = None + self._attr_name = name else: # discovered - self._unique_id = ( + self._attr_unique_id = ( f"{receiver.info['model_name']}_{receiver.info['identifier']}" ) - self._name = self._unique_id + self._attr_name = self._attr_unique_id self._max_volume = max_volume self._receiver_max_volume = receiver_max_volume - self._current_source = None - self._source_list = list(sources.values()) + self._attr_source_list = list(sources.values()) self._source_mapping = sources self._reverse_mapping = {value: key for key, value in sources.items()} - self._attributes = {} + self._attr_extra_state_attributes = {} self._hdmi_out_supported = True self._audio_info_supported = True self._video_info_supported = True @@ -284,9 +282,9 @@ class OnkyoDevice(MediaPlayerEntity): except (ValueError, OSError, AttributeError, AssertionError): if self._receiver.command_socket: self._receiver.command_socket = None - _LOGGER.debug("Resetting connection to %s", self._name) + _LOGGER.debug("Resetting connection to %s", self.name) else: - _LOGGER.info("%s is disconnected. Attempting to reconnect", self._name) + _LOGGER.info("%s is disconnected. Attempting to reconnect", self.name) return False _LOGGER.debug("Result for %s: %s", command, result) return result @@ -298,13 +296,13 @@ class OnkyoDevice(MediaPlayerEntity): if not status: return if status[1] == "on": - self._pwstate = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: - self._pwstate = MediaPlayerState.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) + self._attr_state = MediaPlayerState.OFF + self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_PRESET, None) + self._attr_extra_state_attributes.pop(ATTR_VIDEO_OUT, None) return volume_raw = self.command("volume query") @@ -331,67 +329,27 @@ class OnkyoDevice(MediaPlayerEntity): for source in sources: if source in self._source_mapping: - self._current_source = self._source_mapping[source] + self._attr_source = self._source_mapping[source] break - self._current_source = "_".join(sources) + self._attr_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: - del self._attributes[ATTR_PRESET] + if preset_raw and self.source and self.source.lower() == "radio": + self._attr_extra_state_attributes[ATTR_PRESET] = preset_raw[1] + elif ATTR_PRESET in self._attr_extra_state_attributes: + del self._attr_extra_state_attributes[ATTR_PRESET] - self._muted = bool(mute_raw[1] == "on") + self._attr_is_volume_muted = bool(mute_raw[1] == "on") # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) - self._volume = volume_raw[1] / ( + self._attr_volume_level = volume_raw[1] / ( self._receiver_max_volume * self._max_volume / 100 ) if not hdmi_out_raw: return - self._attributes[ATTR_VIDEO_OUT] = ",".join(hdmi_out_raw[1]) + self._attr_extra_state_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.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._pwstate - - @property - def volume_level(self): - """Return the volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Return boolean indicating mute status.""" - return self._muted - - @property - def source(self): - """Return the current input source of the device.""" - return self._current_source - - @property - def source_list(self): - """List of available input sources.""" - return self._source_list - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - return self._attributes - def turn_off(self) -> None: """Turn the media player off.""" self.command("system-power standby") @@ -431,13 +389,13 @@ class OnkyoDevice(MediaPlayerEntity): def select_source(self, source: str) -> None: """Set the input source.""" - if source in self._source_list: + if self.source_list and source in self.source_list: source = self._reverse_mapping[source] self.command(f"input-selector {source}") def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play radio station by preset number.""" - source = self._reverse_mapping[self._current_source] + source = self._reverse_mapping[self._attr_source] if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: self.command(f"preset {media_id}") @@ -460,9 +418,9 @@ class OnkyoDevice(MediaPlayerEntity): "output_channels": _tuple_get(values, 5), "output_frequency": _tuple_get(values, 6), } - self._attributes[ATTR_AUDIO_INFORMATION] = info + self._attr_extra_state_attributes[ATTR_AUDIO_INFORMATION] = info else: - self._attributes.pop(ATTR_AUDIO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_AUDIO_INFORMATION, None) def _parse_video_information(self, video_information_raw): values = _parse_onkyo_payload(video_information_raw) @@ -480,9 +438,9 @@ class OnkyoDevice(MediaPlayerEntity): "output_color_depth": _tuple_get(values, 7), "picture_mode": _tuple_get(values, 8), } - self._attributes[ATTR_VIDEO_INFORMATION] = info + self._attr_extra_state_attributes[ATTR_VIDEO_INFORMATION] = info else: - self._attributes.pop(ATTR_VIDEO_INFORMATION, None) + self._attr_extra_state_attributes.pop(ATTR_VIDEO_INFORMATION, None) class OnkyoDeviceZone(OnkyoDevice): @@ -509,9 +467,9 @@ class OnkyoDeviceZone(OnkyoDevice): if not status: return if status[1] == "on": - self._pwstate = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: - self._pwstate = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF return volume_raw = self.command(f"zone{self._zone}.volume=query") @@ -538,22 +496,22 @@ class OnkyoDeviceZone(OnkyoDevice): for source in current_source_tuples[1]: if source in self._source_mapping: - self._current_source = self._source_mapping[source] + self._attr_source = self._source_mapping[source] break - self._current_source = "_".join(current_source_tuples[1]) - self._muted = bool(mute_raw[1] == "on") - if preset_raw and self._current_source.lower() == "radio": - self._attributes[ATTR_PRESET] = preset_raw[1] - elif ATTR_PRESET in self._attributes: - del self._attributes[ATTR_PRESET] + self._attr_source = "_".join(current_source_tuples[1]) + self._attr_is_volume_muted = bool(mute_raw[1] == "on") + if preset_raw and self.source and self.source.lower() == "radio": + self._attr_extra_state_attributes[ATTR_PRESET] = preset_raw[1] + elif ATTR_PRESET in self._attr_extra_state_attributes: + del self._attr_extra_state_attributes[ATTR_PRESET] if self._supports_volume: # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) - self._volume = ( + self._attr_volume_level = ( volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) ) @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Return media player features that are supported.""" if self._supports_volume: return SUPPORT_ONKYO @@ -598,6 +556,6 @@ class OnkyoDeviceZone(OnkyoDevice): def select_source(self, source: str) -> None: """Set the input source.""" - if source in self._source_list: + if self.source_list and source in self.source_list: source = self._reverse_mapping[source] self.command(f"zone{self._zone}.selector={source}") diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 9a8535f2599..11699731b2f 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -137,7 +137,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) -> bytes | None: """Return a still image response from the camera.""" - if self.stream and self.stream.keepalive: + if self.stream and self.stream.dynamic_stream_settings.preload_stream: return await self.stream.async_get_image(width, height) if self.device.capabilities.snapshot: diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index d7f50f6744e..8c000852c48 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -11,7 +11,7 @@ from httpx import RequestError import onvif from onvif import ONVIFCamera from onvif.exceptions import ONVIFError -from zeep.exceptions import Fault +from zeep.exceptions import Fault, XMLParseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -284,7 +284,7 @@ class ONVIFDevice: snapshot = media_capabilities and media_capabilities.SnapshotUri pullpoint = False - with suppress(ONVIFError, Fault, RequestError): + with suppress(ONVIFError, Fault, RequestError, XMLParseError): pullpoint = await self.events.async_start() ptz = False diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 2dd5d226e37..8b09ee99079 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable from contextlib import suppress import datetime as dt +from logging import DEBUG, WARNING from httpx import RemoteProtocolError, TransportError from onvif import ONVIFCamera, ONVIFService @@ -20,7 +21,6 @@ from .parsers import PARSERS UNHANDLED_TOPICS = set() SUBSCRIPTION_ERRORS = ( - XMLParseError, Fault, asyncio.TimeoutError, TransportError, @@ -122,20 +122,32 @@ class EventManager: if self._subscription: # Suppressed. The subscription may no longer exist. - with suppress(*SUBSCRIPTION_ERRORS): + try: await self._subscription.Unsubscribe() + except (XMLParseError, *SUBSCRIPTION_ERRORS) as err: + LOGGER.debug( + "Failed to unsubscribe ONVIF PullPoint subscription for '%s';" + " This is normal if the device restarted: %s", + self.unique_id, + err, + ) self._subscription = None try: restarted = await self.async_start() - except SUBSCRIPTION_ERRORS: + except (XMLParseError, *SUBSCRIPTION_ERRORS) as err: restarted = False + # Device may not support subscriptions so log at debug level + # when we get an XMLParseError + LOGGER.log( + DEBUG if isinstance(err, XMLParseError) else WARNING, + "Failed to restart ONVIF PullPoint subscription for '%s'; " + "Retrying later: %s", + self.unique_id, + err, + ) if not restarted: - LOGGER.warning( - "Failed to restart ONVIF PullPoint subscription for '%s'. Retrying", - self.unique_id, - ) # Try again in a minute self._unsub_refresh = async_call_later(self.hass, 60, self.async_restart) elif self._listeners: @@ -154,8 +166,7 @@ class EventManager: .isoformat(timespec="seconds") .replace("+00:00", "Z") ) - with suppress(*SUBSCRIPTION_ERRORS): - await self._subscription.Renew(termination_time) + await self._subscription.Renew(termination_time) def async_schedule_pull(self) -> None: """Schedule async_pull_messages to run.""" @@ -176,10 +187,13 @@ class EventManager: ).total_seconds() < 7200: await self.async_renew() except RemoteProtocolError: - # Likley a shutdown event, nothing to see here + # Likely a shutdown event, nothing to see here return - except SUBSCRIPTION_ERRORS as err: - LOGGER.warning( + except (XMLParseError, *SUBSCRIPTION_ERRORS) as err: + # Device may not support subscriptions so log at debug level + # when we get an XMLParseError + LOGGER.log( + DEBUG if isinstance(err, XMLParseError) else WARNING, "Failed to fetch ONVIF PullPoint subscription messages for '%s': %s", self.unique_id, err, diff --git a/homeassistant/components/onvif/translations/bg.json b/homeassistant/components/onvif/translations/bg.json index ba0e0dd6277..0fc81f8514f 100644 --- a/homeassistant/components/onvif/translations/bg.json +++ b/homeassistant/components/onvif/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "configure": { "data": { diff --git a/homeassistant/components/onvif/translations/cs.json b/homeassistant/components/onvif/translations/cs.json index 100c4eb3788..ebd4166392f 100644 --- a/homeassistant/components/onvif/translations/cs.json +++ b/homeassistant/components/onvif/translations/cs.json @@ -43,7 +43,8 @@ "step": { "onvif_devices": { "data": { - "extra_arguments": "Dal\u0161\u00ed FFMPEG argumenty" + "extra_arguments": "Dal\u0161\u00ed FFMPEG argumenty", + "use_wallclock_as_timestamps": "Pou\u017eijte n\u00e1st\u011bnn\u00e9 hodiny jako \u010dasov\u00e1 raz\u00edtka" }, "title": "Mo\u017enosti za\u0159\u00edzen\u00ed ONVIF" } diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index 10a7ec2ed53..2eaf20b2b3a 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Zus\u00e4tzliche FFMPEG-Argumente", - "rtsp_transport": "RTSP-Transportmechanismus" + "rtsp_transport": "RTSP-Transportmechanismus", + "use_wallclock_as_timestamps": "Wanduhr als Zeitstempel verwenden" }, "title": "ONVIF-Ger\u00e4teoptionen" } diff --git a/homeassistant/components/onvif/translations/el.json b/homeassistant/components/onvif/translations/el.json index 911994c7344..84d03d2a97e 100644 --- a/homeassistant/components/onvif/translations/el.json +++ b/homeassistant/components/onvif/translations/el.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "\u0395\u03c0\u03b9\u03c0\u03bb\u03ad\u03bf\u03bd \u03bf\u03c1\u03af\u03c3\u03bc\u03b1\u03c4\u03b1 FFMPEG", - "rtsp_transport": "\u039c\u03b7\u03c7\u03b1\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP" + "rtsp_transport": "\u039c\u03b7\u03c7\u03b1\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP", + "use_wallclock_as_timestamps": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c1\u03bf\u03bb\u03bf\u03b3\u03b9\u03bf\u03cd \u03c4\u03bf\u03af\u03c7\u03bf\u03c5 \u03c9\u03c2 \u03c7\u03c1\u03bf\u03bd\u03bf\u03c3\u03c6\u03c1\u03b1\u03b3\u03af\u03b4\u03b5\u03c2" }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 ONVIF" } diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index ce828b53cd5..b20c53988c2 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Argumentos extra de FFMPEG", - "rtsp_transport": "Mecanismo de transporte RTSP" + "rtsp_transport": "Mecanismo de transporte RTSP", + "use_wallclock_as_timestamps": "Usar reloj de pared como marca de tiempo" }, "title": "Opciones del dispositivo ONVIF" } diff --git a/homeassistant/components/onvif/translations/et.json b/homeassistant/components/onvif/translations/et.json index 1d7bede6657..22389a11756 100644 --- a/homeassistant/components/onvif/translations/et.json +++ b/homeassistant/components/onvif/translations/et.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "T\u00e4iendavad FFMPEG argumendid", - "rtsp_transport": "RTSP edastusviis" + "rtsp_transport": "RTSP edastusviis", + "use_wallclock_as_timestamps": "Seinakella kasutamine ajatemplitena" }, "title": "ONVIF-seadme suvandid" } diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index 727457fac34..76a421947d7 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -38,7 +38,7 @@ "data": { "auto": "\u05d7\u05d9\u05e4\u05d5\u05e9 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9" }, - "description": "\u05d1\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e0\u05d7\u05e4\u05e9 \u05d1\u05e8\u05e9\u05ea \u05e9\u05dc\u05da \u05de\u05db\u05e9\u05d9\u05e8\u05d9 ONVIF \u05d4\u05ea\u05d5\u05de\u05db\u05d9\u05dd \u05d1\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc S.\n\n\u05d9\u05e6\u05e8\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d4\u05d7\u05dc\u05d5 \u05dc\u05d4\u05e9\u05d1\u05d9\u05ea \u05d0\u05ea ONVIF \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05e0\u05d0 \u05d5\u05d3\u05d0 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea ONVIF \u05d6\u05de\u05d9\u05e0\u05d4 \u05d1\u05de\u05e6\u05dc\u05de\u05d4 \u05e9\u05dc\u05da.", + "description": "\u05d1\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e0\u05d7\u05e4\u05e9 \u05d1\u05e8\u05e9\u05ea \u05e9\u05dc\u05da \u05d4\u05ea\u05e7\u05e0\u05d9 ONVIF \u05d4\u05ea\u05d5\u05de\u05db\u05d9\u05dd \u05d1\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc S.\n\n\u05d9\u05e6\u05e8\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d4\u05d7\u05dc\u05d5 \u05dc\u05d4\u05e9\u05d1\u05d9\u05ea \u05d0\u05ea ONVIF \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05e0\u05d0 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea ONVIF \u05d6\u05de\u05d9\u05e0\u05d4 \u05d1\u05de\u05e6\u05dc\u05de\u05d4 \u05e9\u05dc\u05da.", "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05ea\u05e7\u05df ONVIF" } } diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index 4fcb62c26fc..fa1b455a1a4 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Extra FFMPEG opci\u00f3k", - "rtsp_transport": "RTSP sz\u00e1ll\u00edt\u00e1si mechanizmus" + "rtsp_transport": "RTSP sz\u00e1ll\u00edt\u00e1si mechanizmus", + "use_wallclock_as_timestamps": "\u00d3ra haszn\u00e1lata id\u0151b\u00e9lyegk\u00e9nt" }, "title": "ONVIF eszk\u00f6z opci\u00f3i" } diff --git a/homeassistant/components/onvif/translations/id.json b/homeassistant/components/onvif/translations/id.json index 383287db875..8f4439fe0ab 100644 --- a/homeassistant/components/onvif/translations/id.json +++ b/homeassistant/components/onvif/translations/id.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Argumen FFMPEG ekstra", - "rtsp_transport": "Mekanisme transport RTSP" + "rtsp_transport": "Mekanisme transport RTSP", + "use_wallclock_as_timestamps": "Gunakan jam dinding sebagai stempel waktu" }, "title": "Opsi Perangkat ONVIF" } diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json index 34d23e2f3fe..5a8e395174b 100644 --- a/homeassistant/components/onvif/translations/it.json +++ b/homeassistant/components/onvif/translations/it.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Argomenti FFMPEG aggiuntivi", - "rtsp_transport": "Meccanismo di trasporto RTSP" + "rtsp_transport": "Meccanismo di trasporto RTSP", + "use_wallclock_as_timestamps": "Usa l'orologio come marca temporale" }, "title": "Opzioni dispositivo ONVIF" } diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json index f76aef12557..67b1da28e8d 100644 --- a/homeassistant/components/onvif/translations/nl.json +++ b/homeassistant/components/onvif/translations/nl.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Extra FFMPEG argumenten", - "rtsp_transport": "RTSP-transportmechanisme" + "rtsp_transport": "RTSP-transportmechanisme", + "use_wallclock_as_timestamps": "Gebruik de lokale tijd voor tijdstempels" }, "title": "ONVIF-apparaatopties" } diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json index a9087ba6be4..7219501f079 100644 --- a/homeassistant/components/onvif/translations/no.json +++ b/homeassistant/components/onvif/translations/no.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Ekstra FFMPEG-argumenter", - "rtsp_transport": "RTSP transportmekanisme" + "rtsp_transport": "RTSP transportmekanisme", + "use_wallclock_as_timestamps": "Bruk veggklokke som tidsstempler" }, "title": "ONVIF enhetsalternativer" } diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json index 2300f6d6041..ae2aa3019bb 100644 --- a/homeassistant/components/onvif/translations/pl.json +++ b/homeassistant/components/onvif/translations/pl.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Dodatkowe argumenty FFMPEG", - "rtsp_transport": "Mechanizm transportu RTSP" + "rtsp_transport": "Mechanizm transportu RTSP", + "use_wallclock_as_timestamps": "U\u017cyj wallclock jako znacznika czasu" }, "title": "Opcje urz\u0105dzenia ONVIF" } diff --git a/homeassistant/components/onvif/translations/pt-BR.json b/homeassistant/components/onvif/translations/pt-BR.json index f491f21d56b..41ab5180ddc 100644 --- a/homeassistant/components/onvif/translations/pt-BR.json +++ b/homeassistant/components/onvif/translations/pt-BR.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Argumentos FFMPEG extras", - "rtsp_transport": "Mecanismo de transporte RTSP" + "rtsp_transport": "Mecanismo de transporte RTSP", + "use_wallclock_as_timestamps": "Use o rel\u00f3gio de parede como carimbo de data/hora" }, "title": "Op\u00e7\u00f5es do dispositivo ONVIF" } diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json index d9b09be4d42..1b30aefb519 100644 --- a/homeassistant/components/onvif/translations/ru.json +++ b/homeassistant/components/onvif/translations/ru.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b FFMPEG", - "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c RTSP" + "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c RTSP", + "use_wallclock_as_timestamps": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0435\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u044b \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u043a" }, "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" } diff --git a/homeassistant/components/onvif/translations/sk.json b/homeassistant/components/onvif/translations/sk.json index f51c7e32bc8..f0230e2e07c 100644 --- a/homeassistant/components/onvif/translations/sk.json +++ b/homeassistant/components/onvif/translations/sk.json @@ -4,13 +4,23 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "configure": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov", + "password": "Heslo", "port": "Port", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } + }, + "user": { + "data": { + "auto": "Automatick\u00e9 vyh\u013ead\u00e1vanie" + } } } } diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index f4d2ddf036d..c16195007f4 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "\u984d\u5916 FFMPEG \u53c3\u6578", - "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a" + "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a", + "use_wallclock_as_timestamps": "\u4f7f\u7528\u6642\u9418\u4f5c\u70ba\u6642\u9593\u6233" }, "title": "ONVIF \u88dd\u7f6e\u9078\u9805" } diff --git a/homeassistant/components/open_meteo/translations/es.json b/homeassistant/components/open_meteo/translations/es.json index 87bc3b879be..f8cde4cbba9 100644 --- a/homeassistant/components/open_meteo/translations/es.json +++ b/homeassistant/components/open_meteo/translations/es.json @@ -5,7 +5,7 @@ "data": { "zone": "Zona" }, - "description": "Selecciona la ubicaci\u00f3n que se usar\u00e1 para el pron\u00f3stico del tiempo" + "description": "Selecciona la ubicaci\u00f3n que se usar\u00e1 para la previsi\u00f3n meteorol\u00f3gica" } } } diff --git a/homeassistant/components/open_meteo/translations/sk.json b/homeassistant/components/open_meteo/translations/sk.json new file mode 100644 index 00000000000..28ab47956fa --- /dev/null +++ b/homeassistant/components/open_meteo/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Vyberte miesto, ktor\u00e9 chcete pou\u017ei\u0165 na predpove\u010f po\u010dasia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 6af9900ec15..614aed6eefb 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -5,11 +5,7 @@ from open_meteo import Forecast as OpenMeteoForecast from homeassistant.components.weather import Forecast, WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_MILLIMETERS, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, -) +from homeassistant.const import UnitOfPrecipitationDepth, UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -38,9 +34,9 @@ class OpenMeteoWeatherEntity( """Defines an Open-Meteo weather entity.""" _attr_has_entity_name = True - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR def __init__( self, diff --git a/homeassistant/components/openalpr_local/translations/de.json b/homeassistant/components/openalpr_local/translations/de.json index d517fe0b37f..5b5dfb52773 100644 --- a/homeassistant/components/openalpr_local/translations/de.json +++ b/homeassistant/components/openalpr_local/translations/de.json @@ -2,7 +2,7 @@ "issues": { "pending_removal": { "description": "Die lokale OpenALPR-Integration wird derzeit aus dem Home Assistant entfernt und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die lokale OpenALPR-Integration wird entfernt" + "title": "Die lokale OpenALPR Integration wird entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json index 81d359599ae..f3f3a0aad04 100644 --- a/homeassistant/components/openexchangerates/translations/ca.json +++ b/homeassistant/components/openexchangerates/translations/ca.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "La configuraci\u00f3 d'Open Exchange Rates mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nElimina la configuraci\u00f3 YAML d'Open Exchange Rates del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "La configuraci\u00f3 YAML d'Open Exchange Rates s'ha eliminat" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/de.json b/homeassistant/components/openexchangerates/translations/de.json index a0f974d3374..265147d31da 100644 --- a/homeassistant/components/openexchangerates/translations/de.json +++ b/homeassistant/components/openexchangerates/translations/de.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Das Konfigurieren von Open Exchange Rates mit YAML wurde entfernt. \n\nEntferne die YAML-Konfiguration f\u00fcr Open Exchange Rates aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Open Exchange Rates YAML-Konfiguration wurde entfernt" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/el.json b/homeassistant/components/openexchangerates/translations/el.json index dee7e836d01..59c0501a38d 100644 --- a/homeassistant/components/openexchangerates/translations/el.json +++ b/homeassistant/components/openexchangerates/translations/el.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b1\u03bd\u03bf\u03b9\u03ba\u03c4\u03ce\u03bd \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03b9\u03ce\u03bd \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03ac\u03b3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Open Exchange Rates YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Open Exchange Rates YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/en.json b/homeassistant/components/openexchangerates/translations/en.json index f4827c4df4d..eb41ae0ca14 100644 --- a/homeassistant/components/openexchangerates/translations/en.json +++ b/homeassistant/components/openexchangerates/translations/en.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Configuring Open Exchange Rates using YAML has been removed.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Open Exchange Rates YAML configuration has been removed" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/es.json b/homeassistant/components/openexchangerates/translations/es.json index b71ef652770..982035eb782 100644 --- a/homeassistant/components/openexchangerates/translations/es.json +++ b/homeassistant/components/openexchangerates/translations/es.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Se ha eliminado la configuraci\u00f3n de Open Exchange Rates mediante YAML. \n\nElimina la configuraci\u00f3n YAML de Open Exchange Rates de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se ha eliminado la configuraci\u00f3n YAML de Open Exchange Rates" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/et.json b/homeassistant/components/openexchangerates/translations/et.json index 45ed7fb1cfc..ebe4d41ab69 100644 --- a/homeassistant/components/openexchangerates/translations/et.json +++ b/homeassistant/components/openexchangerates/translations/et.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Open Exchange Rates konfigureerimine YAML-i abil eemaldati.\n\nEemalda Open Exchange Rates YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivita Home Assistant uuesti, et see probleem lahendada.", - "title": "Open Exchange Rates YAML-konfiguratsioon eemaldati" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/fr.json b/homeassistant/components/openexchangerates/translations/fr.json index c6d0bb24444..a6b5929245a 100644 --- a/homeassistant/components/openexchangerates/translations/fr.json +++ b/homeassistant/components/openexchangerates/translations/fr.json @@ -23,10 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "title": "La configuration YAML pour Open Exchange Rates a \u00e9t\u00e9 supprim\u00e9e" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/he.json b/homeassistant/components/openexchangerates/translations/he.json new file mode 100644 index 00000000000..34122738a71 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/hu.json b/homeassistant/components/openexchangerates/translations/hu.json index 51843cd899a..2bd735e177c 100644 --- a/homeassistant/components/openexchangerates/translations/hu.json +++ b/homeassistant/components/openexchangerates/translations/hu.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Az Open Exchange Rates konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "Az Open Exchange Rates YAML-konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json index d53a8e9ef5f..e59732c319f 100644 --- a/homeassistant/components/openexchangerates/translations/id.json +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Proses konfigurasi Integrasi Open Exchange Rates lewat YAML telah dihapus.\n\nHapus konfigurasi YAML Integrasi Open Exchange Rates dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Integrasi Open Exchange Rates telah dihapus" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/it.json b/homeassistant/components/openexchangerates/translations/it.json index 491a44e7508..5547fa57ba1 100644 --- a/homeassistant/components/openexchangerates/translations/it.json +++ b/homeassistant/components/openexchangerates/translations/it.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "La configurazione di Open Exchange Rates tramite YAML \u00e8 stata rimossa. \n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Open Exchange Rates \u00e8 stata rimossa" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json index 9c7212a44b5..01f62ccd060 100644 --- a/homeassistant/components/openexchangerates/translations/ja.json +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Open Exchange Rates\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Open Exchange Rates\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "title": "Open Exchange Rates YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/no.json b/homeassistant/components/openexchangerates/translations/no.json index 1e810f5a52e..6fdbbcdd598 100644 --- a/homeassistant/components/openexchangerates/translations/no.json +++ b/homeassistant/components/openexchangerates/translations/no.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av \u00e5pne valutakurser med YAML er fjernet. \n\n Fjern Open Exchange Rates YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "Open Exchange Rates YAML-konfigurasjonen er fjernet" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/pl.json b/homeassistant/components/openexchangerates/translations/pl.json index a9bb2278d90..48e289f5d2e 100644 --- a/homeassistant/components/openexchangerates/translations/pl.json +++ b/homeassistant/components/openexchangerates/translations/pl.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfiguracja Open Exchange Rates przy u\u017cyciu YAML zosta\u0142a usuni\u0119ta. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Konfiguracja YAML dla Open Exchange Rates zosta\u0142a usuni\u0119ta" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/pt-BR.json b/homeassistant/components/openexchangerates/translations/pt-BR.json index 7f6edf42577..d2cf35cbf63 100644 --- a/homeassistant/components/openexchangerates/translations/pt-BR.json +++ b/homeassistant/components/openexchangerates/translations/pt-BR.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o de Open Exchange Rates usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Open Exchange Rates do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o de YAML de Open Exchange Rates est\u00e1 sendo removida" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/pt.json b/homeassistant/components/openexchangerates/translations/pt.json index 1da8a0cc5ab..18a9b3af81a 100644 --- a/homeassistant/components/openexchangerates/translations/pt.json +++ b/homeassistant/components/openexchangerates/translations/pt.json @@ -10,11 +10,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o de taxas de c\u00e2mbio abertas usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Open Exchange Rates YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o de YAML de taxas de c\u00e2mbio abertas est\u00e1 sendo removida" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ru.json b/homeassistant/components/openexchangerates/translations/ru.json index cfc8edb0e8d..c6f19823ba7 100644 --- a/homeassistant/components/openexchangerates/translations/ru.json +++ b/homeassistant/components/openexchangerates/translations/ru.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Open Exchange Rates\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/sk.json b/homeassistant/components/openexchangerates/translations/sk.json new file mode 100644 index 00000000000..7a67ae4ec9b --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/sv.json b/homeassistant/components/openexchangerates/translations/sv.json index 578d74864ba..eaa56b5b38d 100644 --- a/homeassistant/components/openexchangerates/translations/sv.json +++ b/homeassistant/components/openexchangerates/translations/sv.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Open Exchange Rates med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Open Exchange Rates YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Open Exchange Rates YAML-konfigurationen tas bort" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/tr.json b/homeassistant/components/openexchangerates/translations/tr.json index 4149d5bd52d..cce9561913f 100644 --- a/homeassistant/components/openexchangerates/translations/tr.json +++ b/homeassistant/components/openexchangerates/translations/tr.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "YAML kullanarak A\u00e7\u0131k D\u00f6viz Kurlar\u0131n\u0131 yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Open Exchange Rates YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "A\u00e7\u0131k D\u00f6viz Kurlar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" - } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/zh-Hant.json b/homeassistant/components/openexchangerates/translations/zh-Hant.json index d2b9b7ecb27..e24c925086b 100644 --- a/homeassistant/components/openexchangerates/translations/zh-Hant.json +++ b/homeassistant/components/openexchangerates/translations/zh-Hant.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Open Exchange Rates \u5df2\u79fb\u9664\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Open Exchange Rates YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Open Exchange Rates YAML \u8a2d\u5b9a\u5df2\u79fb\u9664" - } } } \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/sk.json b/homeassistant/components/opengarage/translations/sk.json index 1145b3bb9f8..ad1df6303e0 100644 --- a/homeassistant/components/opengarage/translations/sk.json +++ b/homeassistant/components/opengarage/translations/sk.json @@ -1,12 +1,19 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "user": { "data": { - "port": "Port" + "device_key": "K\u013e\u00fa\u010d zariadenia", + "host": "Hostite\u013e", + "port": "Port", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index f352d7101ac..fba397c1326 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -124,7 +124,7 @@ class OpenhomeDevice(MediaPlayerEntity): self._source_index = {} self._source = {} self._name = None - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._available = True @property @@ -178,16 +178,16 @@ class OpenhomeDevice(MediaPlayerEntity): ) if self._in_standby: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF elif self._transport_state == "Paused": - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED elif self._transport_state in ("Playing", "Buffering"): - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING elif self._transport_state == "Stopped": - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE else: # Device is playing an external source with no transport controls - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self._available = True except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): @@ -279,11 +279,6 @@ class OpenhomeDevice(MediaPlayerEntity): """Return a unique ID.""" return self._device.uuid() - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def source_list(self): """List of available input sources.""" diff --git a/homeassistant/components/opentherm_gw/translations/sk.json b/homeassistant/components/opentherm_gw/translations/sk.json index e7a2eaabb7b..2de0fd19743 100644 --- a/homeassistant/components/opentherm_gw/translations/sk.json +++ b/homeassistant/components/opentherm_gw/translations/sk.json @@ -1,11 +1,28 @@ { "config": { + "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "id_exists": "ID br\u00e1ny u\u017e existuje", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia" + }, "step": { "init": { "data": { + "device": "Cesta alebo URL", + "id": "ID", "name": "N\u00e1zov" } } } + }, + "options": { + "step": { + "init": { + "data": { + "set_precision": "Nastavi\u0165 presnos\u0165" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index fb649bce759..3e65f33d8c5 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -5,9 +5,8 @@ import asyncio from typing import Any from pyopenuv import Client -import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, @@ -17,17 +16,10 @@ from homeassistant.const import ( CONF_SENSORS, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - aiohttp_client, - config_validation as cv, - entity_registry, -) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import EntityDescription -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.service import verify_domain_control -from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( CONF_FROM_WINDOW, @@ -39,84 +31,13 @@ from .const import ( DOMAIN, LOGGER, ) -from .coordinator import OpenUvCoordinator - -CONF_ENTRY_ID = "entry_id" +from .coordinator import InvalidApiKeyMonitor, OpenUvCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -SERVICE_NAME_UPDATE_DATA = "update_data" -SERVICE_NAME_UPDATE_PROTECTION_DATA = "update_protection_data" -SERVICE_NAME_UPDATE_UV_INDEX_DATA = "update_uv_index_data" - -SERVICES = ( - SERVICE_NAME_UPDATE_DATA, - SERVICE_NAME_UPDATE_PROTECTION_DATA, - SERVICE_NAME_UPDATE_UV_INDEX_DATA, -) - - -@callback -def async_get_entity_id_from_unique_id_suffix( - hass: HomeAssistant, entry: ConfigEntry, unique_id_suffix: str -) -> str: - """Get the entity ID for a config entry based on unique ID suffix.""" - ent_reg = entity_registry.async_get(hass) - [registry_entry] = [ - registry_entry - for registry_entry in ent_reg.entities.values() - if registry_entry.config_entry_id == entry.entry_id - and registry_entry.unique_id.endswith(unique_id_suffix) - ] - return registry_entry.entity_id - - -@callback -def async_log_deprecated_service_call( - hass: HomeAssistant, - call: ServiceCall, - alternate_service: str, - alternate_targets: list[str], - breaks_in_ha_version: str, -) -> None: - """Log a warning about a deprecated service call.""" - deprecated_service = f"{call.domain}.{call.service}" - - if len(alternate_targets) > 1: - translation_key = "deprecated_service_multiple_alternate_targets" - else: - translation_key = "deprecated_service_single_alternate_target" - - async_create_issue( - hass, - DOMAIN, - f"deprecated_service_{deprecated_service}", - breaks_in_ha_version=breaks_in_ha_version, - is_fixable=False, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "alternate_service": alternate_service, - "alternate_targets": ", ".join(alternate_targets), - "deprecated_service": deprecated_service, - }, - ) - - LOGGER.warning( - ( - 'The "%s" service is deprecated and will be removed in %s; review the ' - "Repairs item in the UI for more information" - ), - deprecated_service, - breaks_in_ha_version, - ) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" - _verify_domain_control = verify_domain_control(hass, DOMAIN) - websession = aiohttp_client.async_get_clientsession(hass) client = Client( entry.data[CONF_API_KEY], @@ -132,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) return await client.uv_protection_window(low=low, high=high) + invalid_api_key_monitor = InvalidApiKeyMonitor(hass, entry) + coordinators: dict[str, OpenUvCoordinator] = { coordinator_name: OpenUvCoordinator( hass, @@ -139,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: latitude=client.latitude, longitude=client.longitude, update_method=update_method, + invalid_api_key_monitor=invalid_api_key_monitor, ) for coordinator_name, update_method in ( (DATA_UV, client.uv_index), @@ -162,81 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - # We determine entity IDs needed to help the user migrate from deprecated services: - current_uv_index_entity_id = async_get_entity_id_from_unique_id_suffix( - hass, entry, "current_uv_index" - ) - protection_window_entity_id = async_get_entity_id_from_unique_id_suffix( - hass, entry, "protection_window" - ) - - @_verify_domain_control - async def update_data(call: ServiceCall) -> None: - """Refresh all OpenUV data.""" - LOGGER.debug("Refreshing all OpenUV data") - async_log_deprecated_service_call( - hass, - call, - "homeassistant.update_entity", - [protection_window_entity_id, current_uv_index_entity_id], - "2022.12.0", - ) - - tasks = [coordinator.async_refresh() for coordinator in coordinators.values()] - try: - await asyncio.gather(*tasks) - except UpdateFailed as err: - raise HomeAssistantError(err) from err - - @_verify_domain_control - async def update_uv_index_data(call: ServiceCall) -> None: - """Refresh OpenUV UV index data.""" - LOGGER.debug("Refreshing OpenUV UV index data") - async_log_deprecated_service_call( - hass, - call, - "homeassistant.update_entity", - [current_uv_index_entity_id], - "2022.12.0", - ) - - try: - await coordinators[DATA_UV].async_request_refresh() - except UpdateFailed as err: - raise HomeAssistantError(err) from err - - @_verify_domain_control - async def update_protection_data(call: ServiceCall) -> None: - """Refresh OpenUV protection window data.""" - LOGGER.debug("Refreshing OpenUV protection window data") - async_log_deprecated_service_call( - hass, - call, - "homeassistant.update_entity", - [protection_window_entity_id], - "2022.12.0", - ) - - try: - await coordinators[DATA_PROTECTION_WINDOW].async_request_refresh() - except UpdateFailed as err: - raise HomeAssistantError(err) from err - - service_schema = vol.Schema( - { - vol.Optional(CONF_ENTRY_ID, default=entry.entry_id): cv.string, - } - ) - - for service, method in ( - (SERVICE_NAME_UPDATE_DATA, update_data), - (SERVICE_NAME_UPDATE_UV_INDEX_DATA, update_uv_index_data), - (SERVICE_NAME_UPDATE_PROTECTION_DATA, update_protection_data), - ): - if hass.services.has_service(DOMAIN, service): - continue - hass.services.async_register(DOMAIN, service, method, schema=service_schema) - return True @@ -246,17 +95,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - loaded_entries = [ - entry - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED - ] - if len(loaded_entries) == 1: - # If this is the last loaded instance of OpenUV, deregister any services - # defined during integration setup: - for service_name in SERVICES: - hass.services.async_remove(DOMAIN, service_name) - return unload_ok diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index facbc37986e..3951a6ffb08 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the OpenUV component.""" from __future__ import annotations +from collections.abc import Mapping +from dataclasses import dataclass from typing import Any from pyopenuv import Client @@ -18,6 +20,10 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) from .const import ( CONF_FROM_WINDOW, @@ -27,14 +33,54 @@ from .const import ( DOMAIN, ) +STEP_REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } +) + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional( + CONF_FROM_WINDOW, description={"suggested_value": DEFAULT_FROM_WINDOW} + ): vol.Coerce(float), + vol.Optional( + CONF_TO_WINDOW, description={"suggested_value": DEFAULT_TO_WINDOW} + ): vol.Coerce(float), + } +) + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + + +@dataclass +class OpenUvData: + """Define structured OpenUV data needed to create/re-auth an entry.""" + + api_key: str + latitude: float + longitude: float + elevation: float + + @property + def unique_id(self) -> str: + """Return the unique for this data.""" + return f"{self.latitude}, {self.longitude}" + class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an OpenUV config flow.""" VERSION = 2 + def __init__(self) -> None: + """Initialize.""" + self._reauth_data: Mapping[str, Any] = {} + @property - def config_schema(self) -> vol.Schema: + def step_user_schema(self) -> vol.Schema: """Return the config schema.""" return vol.Schema( { @@ -51,76 +97,93 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: - """Show the form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=self.config_schema, - errors=errors if errors else {}, - ) + async def _async_verify( + self, data: OpenUvData, error_step_id: str, error_schema: vol.Schema + ) -> FlowResult: + """Verify the credentials and create/re-auth the entry.""" + websession = aiohttp_client.async_get_clientsession(self.hass) + client = Client(data.api_key, 0, 0, session=websession) + client.disable_request_retries() + + try: + await client.uv_index() + except OpenUvError: + return self.async_show_form( + step_id=error_step_id, + data_schema=error_schema, + errors={CONF_API_KEY: "invalid_api_key"}, + description_placeholders={ + CONF_LATITUDE: str(data.latitude), + CONF_LONGITUDE: str(data.longitude), + }, + ) + + entry_data = { + CONF_API_KEY: data.api_key, + CONF_LATITUDE: data.latitude, + CONF_LONGITUDE: data.longitude, + CONF_ELEVATION: data.elevation, + } + + if existing_entry := await self.async_set_unique_id(data.unique_id): + self.hass.config_entries.async_update_entry(existing_entry, data=entry_data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=data.unique_id, data=entry_data) @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> OpenUvOptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: """Define the config flow to handle options.""" - return OpenUvOptionsFlowHandler(config_entry) + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauth_data = entry_data + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_REAUTH_SCHEMA, + description_placeholders={ + CONF_LATITUDE: self._reauth_data[CONF_LATITUDE], + CONF_LONGITUDE: self._reauth_data[CONF_LONGITUDE], + }, + ) + + data = OpenUvData( + user_input[CONF_API_KEY], + self._reauth_data[CONF_LATITUDE], + self._reauth_data[CONF_LONGITUDE], + self._reauth_data[CONF_ELEVATION], + ) + + return await self._async_verify(data, "reauth_confirm", STEP_REAUTH_SCHEMA) async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: - return await self._show_form() + return self.async_show_form( + step_id="user", data_schema=self.step_user_schema + ) - identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" - await self.async_set_unique_id(identifier) + data = OpenUvData( + user_input[CONF_API_KEY], + user_input[CONF_LATITUDE], + user_input[CONF_LONGITUDE], + user_input[CONF_ELEVATION], + ) + + await self.async_set_unique_id(data.unique_id) self._abort_if_unique_id_configured() - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(user_input[CONF_API_KEY], 0, 0, session=websession) - - try: - await client.uv_index() - except OpenUvError: - return await self._show_form({CONF_API_KEY: "invalid_api_key"}) - - return self.async_create_entry(title=identifier, data=user_input) - - -class OpenUvOptionsFlowHandler(config_entries.OptionsFlow): - """Handle a OpenUV options flow.""" - - def __init__(self, entry: ConfigEntry) -> None: - """Initialize.""" - self.entry = entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_FROM_WINDOW, - description={ - "suggested_value": self.entry.options.get( - CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW - ) - }, - ): vol.Coerce(float), - vol.Optional( - CONF_TO_WINDOW, - description={ - "suggested_value": self.entry.options.get( - CONF_FROM_WINDOW, DEFAULT_TO_WINDOW - ) - }, - ): vol.Coerce(float), - } - ), - ) + return await self._async_verify(data, "user", self.step_user_schema) diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index 993970658ef..f89a9c696a8 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -1,12 +1,15 @@ """Define an update coordinator for OpenUV.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from typing import Any, cast -from pyopenuv.errors import OpenUvError +from pyopenuv.errors import InvalidApiKeyError, OpenUvError -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -15,9 +18,68 @@ from .const import LOGGER DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 +class InvalidApiKeyMonitor: + """Define a monitor for failed API calls (due to bad keys) across coordinators.""" + + DEFAULT_FAILED_API_CALL_THRESHOLD = 5 + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self._count = 1 + self._lock = asyncio.Lock() + self._reauth_flow_manager = ReauthFlowManager(hass, entry) + self.entry = entry + + async def async_increment(self) -> None: + """Increment the counter.""" + async with self._lock: + self._count += 1 + if self._count > self.DEFAULT_FAILED_API_CALL_THRESHOLD: + LOGGER.info("Starting reauth after multiple failed API calls") + self._reauth_flow_manager.start_reauth() + + async def async_reset(self) -> None: + """Reset the counter.""" + async with self._lock: + self._count = 0 + self._reauth_flow_manager.cancel_reauth() + + +class ReauthFlowManager: + """Define an OpenUV reauth flow manager.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self.entry = entry + self.hass = hass + + @callback + def _get_active_reauth_flow(self) -> FlowResult | None: + """Get an active reauth flow (if it exists).""" + return next( + iter(self.entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})), + None, + ) + + @callback + def cancel_reauth(self) -> None: + """Cancel a reauth flow (if appropriate).""" + if reauth_flow := self._get_active_reauth_flow(): + LOGGER.debug("API seems to have recovered; canceling reauth flow") + self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"]) + + @callback + def start_reauth(self) -> None: + """Start a reauth flow (if appropriate).""" + if not self._get_active_reauth_flow(): + LOGGER.debug("Multiple API failures in a row; starting reauth flow") + self.entry.async_start_reauth(self.hass) + + class OpenUvCoordinator(DataUpdateCoordinator): """Define an OpenUV data coordinator.""" + config_entry: ConfigEntry update_method: Callable[[], Awaitable[dict[str, Any]]] def __init__( @@ -28,6 +90,7 @@ class OpenUvCoordinator(DataUpdateCoordinator): latitude: str, longitude: str, update_method: Callable[[], Awaitable[dict[str, Any]]], + invalid_api_key_monitor: InvalidApiKeyMonitor, ) -> None: """Initialize.""" super().__init__( @@ -43,6 +106,7 @@ class OpenUvCoordinator(DataUpdateCoordinator): ), ) + self._invalid_api_key_monitor = invalid_api_key_monitor self.latitude = latitude self.longitude = longitude @@ -50,6 +114,11 @@ class OpenUvCoordinator(DataUpdateCoordinator): """Fetch data from OpenUV.""" try: data = await self.update_method() + except InvalidApiKeyError as err: + await self._invalid_api_key_monitor.async_increment() + raise UpdateFailed(str(err)) from err except OpenUvError as err: - raise UpdateFailed(f"Error during protection data update: {err}") from err + raise UpdateFailed(str(err)) from err + + await self._invalid_api_key_monitor.async_reset() return cast(dict[str, Any], data["result"]) diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml deleted file mode 100644 index 3e2e6ab0087..00000000000 --- a/homeassistant/components/openuv/services.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Describes the format for available OpenUV services -update_data: - name: Update data - description: Request new data from OpenUV. Consumes two API calls. - fields: - entry_id: - name: Config Entry - description: The configured instance of the OpenUV integration to use - required: true - selector: - config_entry: - integration: openuv - -update_uv_index_data: - name: Update UV index data - description: Request new UV index data from OpenUV. - fields: - entry_id: - name: Config Entry - description: The configured instance of the OpenUV integration to use - required: true - selector: - config_entry: - integration: openuv - -update_protection_data: - name: Update protection data - description: Request new protection window data from OpenUV. - fields: - entry_id: - name: Config Entry - description: The configured instance of the OpenUV integration to use - required: true - selector: - config_entry: - integration: openuv diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index 84a093280f3..9542cb8b1a7 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -1,6 +1,13 @@ { "config": { "step": { + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please re-enter the API key for {latitude}, {longitude}.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, "user": { "title": "Fill in your information", "data": { @@ -15,7 +22,8 @@ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/openuv/translations/bg.json b/homeassistant/components/openuv/translations/bg.json index 6959a04bb7a..64e6e35e737 100644 --- a/homeassistant/components/openuv/translations/bg.json +++ b/homeassistant/components/openuv/translations/bg.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e API \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 {latitude}, {longitude}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447 \u0437\u0430 OpenUV", diff --git a/homeassistant/components/openuv/translations/ca.json b/homeassistant/components/openuv/translations/ca.json index 36043c3bde3..b7a7f3e7630 100644 --- a/homeassistant/components/openuv/translations/ca.json +++ b/homeassistant/components/openuv/translations/ca.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_api_key": "Clau API inv\u00e0lida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "Si us plau, torna a introduir la clau API de {latitude}, {longitude}.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/openuv/translations/de.json b/homeassistant/components/openuv/translations/de.json index 94d8b49b7d5..c38c2d33cc8 100644 --- a/homeassistant/components/openuv/translations/de.json +++ b/homeassistant/components/openuv/translations/de.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Standort ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Bitte gib den API-Schl\u00fcssel f\u00fcr {latitude}, {longitude} erneut ein.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", @@ -20,7 +28,7 @@ }, "issues": { "deprecated_service_multiple_alternate_targets": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer dieser Entit\u00e4ts-IDs als Ziel zu verwenden: `{alternate_targets}`.", + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer dieser Entit\u00e4ts IDs als Ziel zu verwenden: `{alternate_targets}`.", "title": "Der Dienst {deprecated_service} wird entfernt" }, "deprecated_service_single_alternate_target": { diff --git a/homeassistant/components/openuv/translations/el.json b/homeassistant/components/openuv/translations/el.json index 0b81ff25fd5..7a88486863b 100644 --- a/homeassistant/components/openuv/translations/el.json +++ b/homeassistant/components/openuv/translations/el.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b3\u03b9\u03b1 \u03c4\u03b1 {latitude}, {longitude}.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/openuv/translations/en.json b/homeassistant/components/openuv/translations/en.json index 3879a4d7d44..9db83868543 100644 --- a/homeassistant/components/openuv/translations/en.json +++ b/homeassistant/components/openuv/translations/en.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Location is already configured" + "already_configured": "Location is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_api_key": "Invalid API key" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "Please re-enter the API key for {latitude}, {longitude}.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "api_key": "API Key", diff --git a/homeassistant/components/openuv/translations/es.json b/homeassistant/components/openuv/translations/es.json index 66331b8e5a5..363ccc82ee0 100644 --- a/homeassistant/components/openuv/translations/es.json +++ b/homeassistant/components/openuv/translations/es.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_api_key": "Clave API no v\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "description": "Por favor, vuelve a introducir la clave API para {latitude}, {longitude}.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "api_key": "Clave API", diff --git a/homeassistant/components/openuv/translations/et.json b/homeassistant/components/openuv/translations/et.json index 76145f40ed0..4f6fca0ec01 100644 --- a/homeassistant/components/openuv/translations/et.json +++ b/homeassistant/components/openuv/translations/et.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_api_key": "Vigane API v\u00f5ti" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Sisesta uuesti API v\u00f5ti {latitude} , {longitude} jaoks.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/openuv/translations/fr.json b/homeassistant/components/openuv/translations/fr.json index 527d4d3348e..5485f0b4ac6 100644 --- a/homeassistant/components/openuv/translations/fr.json +++ b/homeassistant/components/openuv/translations/fr.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "description": "Veuillez de nouveau saisir la cl\u00e9 d\u2019API pour {latitude}, {longitude}.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", diff --git a/homeassistant/components/openuv/translations/hr.json b/homeassistant/components/openuv/translations/hr.json index 835929d26df..e18b0015d51 100644 --- a/homeassistant/components/openuv/translations/hr.json +++ b/homeassistant/components/openuv/translations/hr.json @@ -1,12 +1,17 @@ { "config": { + "error": { + "invalid_api_key": "Neva\u017ee\u0107i API klju\u010d" + }, "step": { "user": { "data": { + "api_key": "API klju\u010d", "elevation": "Elevacija", "latitude": "Zemljopisna \u0161irina", "longitude": "Zemljopisna du\u017eina" - } + }, + "title": "Ispunite svoje podatke" } } } diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index f6493247a99..6e9a7cc4c1f 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "K\u00e9rj\u00fck, adja meg \u00fajra az API-kulcsot a k\u00f6vetkez\u0151h\u00f6z: {latitude}, {longitude}.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 6ad5ee1f8d9..f7ef8c51b69 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" + "already_configured": "Lokasi sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_api_key": "Kunci API tidak valid" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Masukkan kembali kunci API untuk {latitude}, {longitude}.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "api_key": "Kunci API", diff --git a/homeassistant/components/openuv/translations/it.json b/homeassistant/components/openuv/translations/it.json index 4e51b09aec2..f3f4f290017 100644 --- a/homeassistant/components/openuv/translations/it.json +++ b/homeassistant/components/openuv/translations/it.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + "already_configured": "La posizione \u00e8 gi\u00e0 configurata", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_api_key": "Chiave API non valida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "Inserisci nuovamente la chiave API per {latitude}, {longitude}.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json index a85799658e3..8a1a5e0489d 100644 --- a/homeassistant/components/openuv/translations/nl.json +++ b/homeassistant/components/openuv/translations/nl.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd" + "already_configured": "Locatie is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_api_key": "Ongeldige API-sleutel" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/openuv/translations/no.json b/homeassistant/components/openuv/translations/no.json index 1fcba27dc9f..948b1e83369 100644 --- a/homeassistant/components/openuv/translations/no.json +++ b/homeassistant/components/openuv/translations/no.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Plasseringen er allerede konfigurert" + "already_configured": "Plasseringen er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_api_key": "Ugyldig API-n\u00f8kkel" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Vennligst skriv inn API-n\u00f8kkelen for {latitude} , {longitude} .", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/openuv/translations/pl.json b/homeassistant/components/openuv/translations/pl.json index 6578e6fcf84..530b09aaddc 100644 --- a/homeassistant/components/openuv/translations/pl.json +++ b/homeassistant/components/openuv/translations/pl.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "invalid_api_key": "Nieprawid\u0142owy klucz API" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "description": "Wprowad\u017a ponownie klucz API dla {latitude}, {longitude}.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/openuv/translations/pt-BR.json b/homeassistant/components/openuv/translations/pt-BR.json index 9d0c6dd7e8a..c98e76680e7 100644 --- a/homeassistant/components/openuv/translations/pt-BR.json +++ b/homeassistant/components/openuv/translations/pt-BR.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave API" + }, + "description": "Insira novamente a chave de API para {latitude} , {longitude} .", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/openuv/translations/ru.json b/homeassistant/components/openuv/translations/ru.json index ce7da6af147..179707cf96a 100644 --- a/homeassistant/components/openuv/translations/ru.json +++ b/homeassistant/components/openuv/translations/ru.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \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": { "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API \u0434\u043b\u044f {latitude}, {longitude}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/openuv/translations/sk.json b/homeassistant/components/openuv/translations/sk.json index 19eed2a3fe1..a419a801489 100644 --- a/homeassistant/components/openuv/translations/sk.json +++ b/homeassistant/components/openuv/translations/sk.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + }, + "description": "Znova zadajte k\u013e\u00fa\u010d API pre {latitude}, {longitude}.", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", @@ -18,6 +26,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s jedn\u00fdm z t\u00fdchto ID ent\u00edt ako cie\u013eom: `{alternate_targets}`.", + "title": "Slu\u017eba {deprecated_service} sa odstra\u0148uje" + }, + "deprecated_service_single_alternate_target": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s cie\u013eom `{alternate_targets}`.", + "title": "Slu\u017eba {deprecated_service} sa odstra\u0148uje" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/zh-Hant.json b/homeassistant/components/openuv/translations/zh-Hant.json index eaeeec74e3c..d8b06ae7a2a 100644 --- a/homeassistant/components/openuv/translations/zh-Hant.json +++ b/homeassistant/components/openuv/translations/zh-Hant.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_api_key": "API \u91d1\u9470\u7121\u6548" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 {latitude}\u3001{longitude} API \u5bc6\u9470\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/openweathermap/translations/sk.json b/homeassistant/components/openweathermap/translations/sk.json index f14039f810f..5afeabd6deb 100644 --- a/homeassistant/components/openweathermap/translations/sk.json +++ b/homeassistant/components/openweathermap/translations/sk.json @@ -4,14 +4,29 @@ "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" }, "step": { "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", + "language": "Jazyk", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "mode": "Re\u017eim", + "name": "N\u00e1zov" + }, + "description": "Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://openweathermap.org/appid" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Jazyk", + "mode": "Re\u017eim" } } } diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 814dc7dfd02..da29031d513 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -18,10 +18,10 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType @@ -89,10 +89,10 @@ class OpenWeatherMapWeather(WeatherEntity): _attr_attribution = ATTRIBUTION _attr_should_poll = False - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__( self, diff --git a/homeassistant/components/oralb/device.py b/homeassistant/components/oralb/device.py index 0b9da5c3779..3cc46fd27c6 100644 --- a/homeassistant/components/oralb/device.py +++ b/homeassistant/components/oralb/device.py @@ -1,13 +1,11 @@ """Support for OralB devices.""" from __future__ import annotations -from oralb_ble import DeviceKey, SensorDeviceInfo +from oralb_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo def device_key_to_bluetooth_entity_key( @@ -15,17 +13,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a oralb device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 8868330a7e7..94abb85a7b8 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.14.2"], + "requirements": ["oralb-ble==0.14.3"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 6fbc19b092a..af1edd582f9 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -22,9 +22,10 @@ from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TIME_SECONDS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { OralBSensor.TIME: SensorEntityDescription( @@ -67,7 +68,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/oralb/translations/cs.json b/homeassistant/components/oralb/translations/cs.json new file mode 100644 index 00000000000..1163b27775a --- /dev/null +++ b/homeassistant/components/oralb/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "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", + "not_supported": "Za\u0159\u00edzen\u00ed nen\u00ed podporov\u00e1no" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/el.json b/homeassistant/components/oralb/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/oralb/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/he.json b/homeassistant/components/oralb/translations/he.json new file mode 100644 index 00000000000..e34a0c9d525 --- /dev/null +++ b/homeassistant/components/oralb/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/hr.json b/homeassistant/components/oralb/translations/hr.json new file mode 100644 index 00000000000..e592ffe7bb5 --- /dev/null +++ b/homeassistant/components/oralb/translations/hr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "already_in_progress": "Konfiguracije je ve\u0107 u tijeku", + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei", + "not_supported": "Ure\u0111aj nije podr\u017ean" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u017delite li postaviti {name}?" + }, + "user": { + "data": { + "address": "Ure\u0111aj" + }, + "description": "Odaberite ure\u0111aj za postavljanje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/id.json b/homeassistant/components/oralb/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/oralb/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Perangkat tidak didukung" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/it.json b/homeassistant/components/oralb/translations/it.json index b19851b36ee..7784ed3a240 100644 --- a/homeassistant/components/oralb/translations/it.json +++ b/homeassistant/components/oralb/translations/it.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_in_progress": "Il processo di configurazione \u00e8 gi\u00e0 in corso", - "no_devices_found": "Nessun dispositivo trovato in rete", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", "not_supported": "Dispositivo non supportato" }, - "flow_title": "{nome}", + "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "Vuoi impostare {nome}?" + "description": "Vuoi configurare {name}?" }, "user": { "data": { "address": "Dispositivo" }, - "description": "Scegliere un dispositivo da configurare" + "description": "Seleziona un dispositivo da configurare" } } } diff --git a/homeassistant/components/oralb/translations/nl.json b/homeassistant/components/oralb/translations/nl.json new file mode 100644 index 00000000000..02365f4ef8c --- /dev/null +++ b/homeassistant/components/oralb/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat wordt niet ondersteund" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/pt-BR.json b/homeassistant/components/oralb/translations/pt-BR.json index 0da7639fa2a..5b654163201 100644 --- a/homeassistant/components/oralb/translations/pt-BR.json +++ b/homeassistant/components/oralb/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_supported": "Dispositivo n\u00e3o suportado" }, diff --git a/homeassistant/components/oralb/translations/ru.json b/homeassistant/components/oralb/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/oralb/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.", + "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": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/sk.json b/homeassistant/components/oralb/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/oralb/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index e1b204115e6..fa4aca357c1 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -187,7 +187,6 @@ class Luminary(LightEntity): self._changed = changed self._unique_id = None - self._supported_features = [] self._effect_list = [] self._is_on = False self._available = True @@ -205,9 +204,9 @@ class Luminary(LightEntity): """Get a unique ID (not implemented).""" raise NotImplementedError - def _get_supported_color_modes(self): + def _get_supported_color_modes(self) -> set[ColorMode]: """Get supported color modes.""" - color_modes = set() + color_modes: set[ColorMode] = set() if "temp" in self._luminary.supported_features(): color_modes.add(ColorMode.COLOR_TEMP) @@ -222,9 +221,9 @@ class Luminary(LightEntity): return color_modes - def _get_supported_features(self): + def _get_supported_features(self) -> LightEntityFeature: """Get list of supported features.""" - features = 0 + features = LightEntityFeature(0) if "lum" in self._luminary.supported_features(): features = features | LightEntityFeature.TRANSITION @@ -271,11 +270,6 @@ class Luminary(LightEntity): """Return True if the device is on.""" return self._is_on - @property - def supported_features(self): - """List of supported features.""" - return self._supported_features - @property def effect_list(self): """List of supported effects.""" @@ -360,11 +354,11 @@ class Luminary(LightEntity): self._luminary = luminary self.update_static_attributes() - def update_static_attributes(self): + def update_static_attributes(self) -> None: """Update static attributes of the luminary.""" self._unique_id = self._get_unique_id() self._attr_supported_color_modes = self._get_supported_color_modes() - self._supported_features = self._get_supported_features() + self._attr_supported_features = self._get_supported_features() self._effect_list = self._get_effect_list() if ColorMode.COLOR_TEMP in self._attr_supported_color_modes: self._min_mireds = color_util.color_temperature_kelvin_to_mired( @@ -441,11 +435,11 @@ class OsramLightifyGroup(Luminary): # users. return f"{self._luminary.lights()}" - def _get_supported_features(self): + def _get_supported_features(self) -> LightEntityFeature: """Get list of supported features.""" features = super()._get_supported_features() if self._luminary.scenes(): - features = features | LightEntityFeature.EFFECT + features |= LightEntityFeature.EFFECT return features diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py index 2229c583297..ae7d16aee9c 100644 --- a/homeassistant/components/overkiz/alarm_control_panel.py +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -39,7 +39,7 @@ from .entity import OverkizDescriptiveEntity class OverkizAlarmDescriptionMixin: """Define an entity description mixin for switch entities.""" - supported_features: int + supported_features: AlarmControlPanelEntityFeature fn_state: Callable[[Callable[[str], OverkizStateType]], str] diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 32fae234be1..e70315e099d 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -2,15 +2,23 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater +from .atlantic_electrical_heater_with_adjustable_temperature_setpoint import ( + AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint, +) from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation +from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .somfy_thermostat import SomfyThermostat WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, + UIWidget.ATLANTIC_ELECTRICAL_HEATER_WITH_ADJUSTABLE_TEMPERATURE_SETPOINT: AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint, UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: AtlanticHeatRecoveryVentilation, + # ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE works exactly the same as ATLANTIC_PASS_APC_HEATING_ZONE + UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: AtlanticPassAPCHeatingZone, + UIWidget.ATLANTIC_PASS_APC_HEATING_ZONE: AtlanticPassAPCHeatingZone, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, } diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py new file mode 100644 index 00000000000..80c2f418331 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -0,0 +1,134 @@ +"""Support for Atlantic Electrical Heater (With Adjustable Temperature Setpoint).""" +from __future__ import annotations + +from typing import Any + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +PRESET_AUTO = "auto" +PRESET_COMFORT1 = "comfort-1" +PRESET_COMFORT2 = "comfort-2" +PRESET_FROST_PROTECTION = "frost_protection" +PRESET_PROG = "prog" + + +# Map Overkiz presets to Home Assistant presets +OVERKIZ_TO_PRESET_MODE: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_NONE, + OverkizCommandParam.FROSTPROTECTION: PRESET_FROST_PROTECTION, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.COMFORT: PRESET_COMFORT, + OverkizCommandParam.COMFORT_1: PRESET_COMFORT1, + OverkizCommandParam.COMFORT_2: PRESET_COMFORT2, + OverkizCommandParam.AUTO: PRESET_AUTO, + OverkizCommandParam.BOOST: PRESET_BOOST, + OverkizCommandParam.INTERNAL: PRESET_PROG, +} + +PRESET_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODE.items()} + +# Map Overkiz HVAC modes to Home Assistant HVAC modes +OVERKIZ_TO_HVAC_MODE: dict[str, str] = { + OverkizCommandParam.ON: HVACMode.HEAT, + OverkizCommandParam.OFF: HVACMode.OFF, + OverkizCommandParam.AUTO: HVACMode.AUTO, + OverkizCommandParam.BASIC: HVACMode.HEAT, + OverkizCommandParam.STANDBY: HVACMode.OFF, + OverkizCommandParam.INTERNAL: HVACMode.AUTO, +} + +HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()} + +TEMPERATURE_SENSOR_DEVICE_INDEX = 2 + + +class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint( + OverkizEntity, ClimateEntity +): + """Representation of Atlantic Electrical Heater (With Adjustable Temperature Setpoint).""" + + _attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ] + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ) + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self.temperature_device = self.executor.linked_device( + TEMPERATURE_SENSOR_DEVICE_INDEX + ) + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + states = self.device.states + if (state := states[OverkizState.CORE_OPERATING_MODE]) and state.value_as_str: + return OVERKIZ_TO_HVAC_MODE[state.value_as_str] + if (state := states[OverkizState.CORE_ON_OFF]) and state.value_as_str: + return OVERKIZ_TO_HVAC_MODE[state.value_as_str] + return HVACMode.OFF + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + await self.executor.async_execute_command( + OverkizCommand.SET_OPERATING_MODE, HVAC_MODE_TO_OVERKIZ[hvac_mode] + ) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., home, away, temp.""" + if ( + state := self.device.states[OverkizState.IO_TARGET_HEATING_LEVEL] + ) and state.value_as_str: + return OVERKIZ_TO_PRESET_MODE[state.value_as_str] + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode in [PRESET_AUTO, PRESET_PROG]: + command = OverkizCommand.SET_OPERATING_MODE + else: + command = OverkizCommand.SET_HEATING_LEVEL + await self.executor.async_execute_command( + command, PRESET_MODE_TO_OVERKIZ[preset_mode] + ) + + @property + def target_temperature(self) -> float | None: + """Return the temperature.""" + if state := self.device.states[OverkizState.CORE_TARGET_TEMPERATURE]: + return state.value_as_float + return None + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return temperature.value_as_float + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_TEMPERATURE, temperature + ) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py index 7ab59a47a34..374d7e1164d 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -57,7 +57,7 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity): # Not all AtlanticElectricalTowelDryer models support presets, thus we need to check if the command is available if self.executor.has_command(OverkizCommand.SET_TOWEL_DRYER_TEMPORARY_STATE): - self._attr_supported_features += ClimateEntityFeature.PRESET_MODE + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE @property def hvac_mode(self) -> str: diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py new file mode 100644 index 00000000000..3d1f9038356 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py @@ -0,0 +1,203 @@ +"""Support for Atlantic Pass APC Heating Control.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_SLEEP, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +OVERKIZ_TO_HVAC_MODE: dict[str, str] = { + OverkizCommandParam.AUTO: HVACMode.AUTO, + OverkizCommandParam.ECO: HVACMode.AUTO, + OverkizCommandParam.MANU: HVACMode.HEAT, + OverkizCommandParam.HEATING: HVACMode.HEAT, + OverkizCommandParam.STOP: HVACMode.OFF, + OverkizCommandParam.INTERNAL_SCHEDULING: HVACMode.AUTO, + OverkizCommandParam.COMFORT: HVACMode.HEAT, +} + +HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()} + +OVERKIZ_TO_PRESET_MODES: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_ECO, + OverkizCommandParam.STOP: PRESET_ECO, + OverkizCommandParam.MANU: PRESET_COMFORT, + OverkizCommandParam.COMFORT: PRESET_COMFORT, + OverkizCommandParam.ABSENCE: PRESET_AWAY, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.INTERNAL_SCHEDULING: PRESET_HOME, +} + +PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()} + +OVERKIZ_TO_PROFILE_MODES: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_SLEEP, + OverkizCommandParam.STOP: PRESET_SLEEP, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.ABSENCE: PRESET_AWAY, + OverkizCommandParam.MANU: PRESET_COMFORT, + OverkizCommandParam.DEROGATION: PRESET_COMFORT, + OverkizCommandParam.COMFORT: PRESET_COMFORT, +} + +OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: dict[str, str] = { + OverkizCommandParam.ECO: OverkizState.CORE_ECO_HEATING_TARGET_TEMPERATURE, + OverkizCommandParam.COMFORT: OverkizState.CORE_COMFORT_HEATING_TARGET_TEMPERATURE, + OverkizCommandParam.DEROGATION: OverkizState.CORE_DEROGATED_TARGET_TEMPERATURE, +} + + +class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): + """Representation of Atlantic Pass APC Heating Zone Control.""" + + _attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ) + _attr_temperature_unit = TEMP_CELSIUS + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + + # Temperature sensor use the same base_device_url and use the n+1 index + self.temperature_device = self.executor.linked_device( + int(self.index_device_url) + 1 + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return cast(float, temperature.value) + + return None + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return OVERKIZ_TO_HVAC_MODE[ + cast(str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE)) + ] + + @property + def current_heating_profile(self) -> str: + """Return current heating profile.""" + return cast( + str, + self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_PROFILE), + ) + + async def async_set_heating_mode(self, mode: str) -> None: + """Set new heating mode and refresh states.""" + await self.executor.async_execute_command( + OverkizCommand.SET_PASS_APC_HEATING_MODE, mode + ) + + if self.current_heating_profile == OverkizCommandParam.DEROGATION: + # If current mode is in derogation, disable it + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION_ON_OFF_STATE, OverkizCommandParam.OFF + ) + + # We also needs to execute these 2 commands to make it work correctly + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_MODE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_PROFILE + ) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.async_set_heating_mode(HVAC_MODE_TO_OVERKIZ[hvac_mode]) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode]) + + @property + def preset_mode(self) -> str: + """Return the current preset mode, e.g., home, away, temp.""" + heating_mode = cast( + str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE) + ) + + if heating_mode == OverkizCommandParam.INTERNAL_SCHEDULING: + # In Internal scheduling, it could be comfort or eco + return OVERKIZ_TO_PROFILE_MODES[ + cast( + str, + self.executor.select_state( + OverkizState.IO_PASS_APC_HEATING_PROFILE + ), + ) + ] + + return OVERKIZ_TO_PRESET_MODES[heating_mode] + + @property + def target_temperature(self) -> float: + """Return hvac target temperature.""" + current_heating_profile = self.current_heating_profile + if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: + return cast( + float, + self.executor.select_state( + OVERKIZ_TEMPERATURE_STATE_BY_PROFILE[current_heating_profile] + ), + ) + return cast( + float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE) + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + + if self.hvac_mode == HVACMode.AUTO: + await self.executor.async_execute_command( + OverkizCommand.SET_COMFORT_HEATING_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_COMFORT_HEATING_TARGET_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + else: + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATED_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION_ON_OFF_STATE, + OverkizCommandParam.ON, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_MODE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_PROFILE + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index d98709ba2b6..d176a137544 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -32,6 +32,7 @@ PLATFORMS: list[Platform] = [ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.WATER_HEATER, ] IGNORED_OVERKIZ_DEVICES: list[UIClass | UIWidget] = [ @@ -62,10 +63,15 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIClass.WINDOW: Platform.COVER, UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_ELECTRICAL_HEATER_WITH_ADJUSTABLE_TEMPERATURE_SETPOINT: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_PASS_APC_DHW: Platform.WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported) + UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: Platform.WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) + UIWidget.HITACHI_DHW: Platform.WATER_HEATER, # widgetName, uiClass is HitachiHeatingSystem (not supported) UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.MY_FOX_SECURITY_CAMERA: Platform.SWITCH, # widgetName, uiClass is Camera (not supported) UIWidget.RTD_INDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) diff --git a/homeassistant/components/overkiz/cover_entities/awning.py b/homeassistant/components/overkiz/cover_entities/awning.py index 07665c2a5c8..a5aee73bf5b 100644 --- a/homeassistant/components/overkiz/cover_entities/awning.py +++ b/homeassistant/components/overkiz/cover_entities/awning.py @@ -25,9 +25,9 @@ class Awning(OverkizGenericCover): _attr_device_class = CoverDeviceClass.AWNING @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" - supported_features: int = super().supported_features + supported_features = super().supported_features if self.executor.has_command(OverkizCommand.SET_DEPLOYMENT): supported_features |= CoverEntityFeature.SET_POSITION diff --git a/homeassistant/components/overkiz/cover_entities/generic_cover.py b/homeassistant/components/overkiz/cover_entities/generic_cover.py index e36d7a680b1..1bc108b531d 100644 --- a/homeassistant/components/overkiz/cover_entities/generic_cover.py +++ b/homeassistant/components/overkiz/cover_entities/generic_cover.py @@ -129,9 +129,9 @@ class OverkizGenericCover(OverkizEntity, CoverEntity): return attr @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" - supported_features = 0 + supported_features = CoverEntityFeature(0) if self.executor.has_command(*COMMANDS_OPEN_TILT): supported_features |= CoverEntityFeature.OPEN_TILT diff --git a/homeassistant/components/overkiz/cover_entities/vertical_cover.py b/homeassistant/components/overkiz/cover_entities/vertical_cover.py index 90ac6428960..1459786c23f 100644 --- a/homeassistant/components/overkiz/cover_entities/vertical_cover.py +++ b/homeassistant/components/overkiz/cover_entities/vertical_cover.py @@ -46,9 +46,9 @@ class VerticalCover(OverkizGenericCover): """Representation of an Overkiz vertical cover.""" @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" - supported_features: int = super().supported_features + supported_features = super().supported_features if self.executor.has_command(OverkizCommand.SET_CLOSURE): supported_features |= CoverEntityFeature.SET_POSITION diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index c17f30393fc..85e5a3fdf57 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -27,7 +27,10 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): """Initialize the device.""" super().__init__(coordinator) self.device_url = device_url - self.base_device_url, *_ = self.device_url.split("#") + split_device_url = self.device_url.split("#") + self.base_device_url = split_device_url[0] + if len(split_device_url) == 2: + self.index_device_url = split_device_url[1] self.executor = OverkizExecutor(device_url, coordinator) self._attr_assumed_state = not self.device.states diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index e82a6e21f63..7b0f03e446c 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -5,7 +5,7 @@ from typing import Any, cast from urllib.parse import urlparse from pyoverkiz.enums import OverkizCommand, Protocol -from pyoverkiz.models import Command, Device +from pyoverkiz.models import Command, Device, StateDefinition from pyoverkiz.types import StateType as OverkizStateType from .coordinator import OverkizDataUpdateCoordinator @@ -50,6 +50,13 @@ class OverkizExecutor: """Return True if a command exists in a list of commands.""" return self.select_command(*commands) is not None + def select_definition_state(self, *states: str) -> StateDefinition | None: + """Select first existing definition state in a list of states.""" + for existing_state in self.device.definition.states: + if existing_state.qualified_name in states: + return existing_state + return None + def select_state(self, *states: str) -> OverkizStateType: """Select first existing active state in a list of states.""" for state in states: diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index d19495d82a2..49a65b78c06 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -2,8 +2,9 @@ "domain": "overkiz", "name": "Overkiz", "config_flow": true, + "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.5.6"], + "requirements": ["pyoverkiz==1.7.1"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/overkiz/translations/bg.json b/homeassistant/components/overkiz/translations/bg.json index 99fe944f9cb..b15966c0221 100644 --- a/homeassistant/components/overkiz/translations/bg.json +++ b/homeassistant/components/overkiz/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "reauth_wrong_account": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 \u0437\u0430\u043f\u0438\u0441\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0441\u044a\u0441 \u0441\u044a\u0449\u0438\u044f Overkiz \u0430\u043a\u0430\u0443\u043d\u0442 \u0438 \u0445\u044a\u0431 " }, "error": { @@ -11,7 +11,6 @@ "server_in_maintenance": "\u0421\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u0441\u043f\u0440\u044f\u043d \u0437\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430", "too_many_requests": "\u0422\u0432\u044a\u0440\u0434\u0435 \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u044f\u0432\u043a\u0438, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", - "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b. \u0410\u043a\u0430\u0443\u043d\u0442\u0438\u0442\u0435 \u043d\u0430 Somfy Protect \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.", "unsupported_hardware": "\u0412\u0430\u0448\u0438\u044f\u0442 \u0445\u0430\u0440\u0434\u0443\u0435\u0440 {unsupported_device} \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u043e\u0442 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f." }, "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/bn.json b/homeassistant/components/overkiz/translations/bn.json deleted file mode 100644 index de652521c3c..00000000000 --- a/homeassistant/components/overkiz/translations/bn.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "unknown_user": "\u0985\u09aa\u09b0\u09bf\u099a\u09bf\u09a4 \u09ac\u09cd\u09af\u09ac\u09b9\u09be\u09b0\u0995\u09be\u09b0\u09c0\u0964 Somfy Protect \u0985\u09cd\u09af\u09be\u0995\u09be\u0989\u09a8\u09cd\u099f\u0997\u09c1\u09b2\u09bf \u098f\u0987 \u0987\u09a8\u09cd\u099f\u09bf\u0997\u09cd\u09b0\u09c7\u09b6\u09a8 \u09a6\u09cd\u09ac\u09be\u09b0\u09be \u09b8\u09ae\u09b0\u09cd\u09a5\u09bf\u09a4 \u09a8\u09af\u09bc\u0964" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/ca.json b/homeassistant/components/overkiz/translations/ca.json index d1707e0cbf2..fd2cef81b32 100644 --- a/homeassistant/components/overkiz/translations/ca.json +++ b/homeassistant/components/overkiz/translations/ca.json @@ -12,7 +12,6 @@ "too_many_attempts": "Massa intents amb un 'token' inv\u00e0lid, bloquejat temporalment", "too_many_requests": "Massa sol\u00b7licituds, torna-ho a provar m\u00e9s tard", "unknown": "Error inesperat", - "unknown_user": "Usuari desconegut. Els comptes de Somfy Protect no s\u00f3n compatibles amb aquesta integraci\u00f3.", "unsupported_hardware": "{unsupported_device} no \u00e9s compatible amb aquesta integraci\u00f3." }, "flow_title": "Passarel\u00b7la: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/cs.json b/homeassistant/components/overkiz/translations/cs.json index 95baa636720..e82a5515f96 100644 --- a/homeassistant/components/overkiz/translations/cs.json +++ b/homeassistant/components/overkiz/translations/cs.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "unsupported_hardware": "V\u00e1\u0161 hardware {unsupported_device} nen\u00ed touto integrac\u00ed podporov\u00e1n." }, "step": { "user": { diff --git a/homeassistant/components/overkiz/translations/de.json b/homeassistant/components/overkiz/translations/de.json index ff7536e0363..7b69582fe25 100644 --- a/homeassistant/components/overkiz/translations/de.json +++ b/homeassistant/components/overkiz/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Konto wurde bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "reauth_wrong_account": "Du kannst diesen Eintrag nur mit demselben Overkiz-Konto und -Hub erneut authentifizieren" + "reauth_wrong_account": "Du kannst diesen Eintrag nur mit demselben Overkiz Konto und Hub erneut authentifizieren" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -12,7 +12,6 @@ "too_many_attempts": "Zu viele Versuche mit einem ung\u00fcltigen Token, vor\u00fcbergehend gesperrt", "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", "unknown": "Unerwarteter Fehler", - "unknown_user": "Unbekannter Benutzer. Somfy Protect-Konten werden von dieser Integration nicht unterst\u00fctzt.", "unsupported_hardware": "Deine {unsupported_device} Hardware wird von dieser Integration nicht unterst\u00fctzt." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index eb308c470d3..4a61e3ac380 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -12,7 +12,6 @@ "too_many_attempts": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ac \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2", "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", - "unknown_user": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2. \u039f\u03b9 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03af Somfy Protect \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", "unsupported_hardware": "\u03a4\u03bf \u03c5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 {unsupported_device} \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." }, "flow_title": "\u03a0\u03cd\u03bb\u03b7: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index d7dcd2a79ac..2c534a64cb6 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -12,7 +12,6 @@ "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "Unexpected error", - "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration.", "unsupported_hardware": "Your {unsupported_device} hardware is not supported by this integration." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index 1cec438abbb..aae98a32900 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -12,7 +12,6 @@ "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", "unknown": "Error inesperado", - "unknown_user": "Usuario desconocido. Las cuentas de Somfy Protect no son compatibles con esta integraci\u00f3n.", "unsupported_hardware": "Tu hardware {unsupported_device} no es compatible con esta integraci\u00f3n." }, "flow_title": "Puerta de enlace: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/et.json b/homeassistant/components/overkiz/translations/et.json index 2170fff5c45..1e34227b0d1 100644 --- a/homeassistant/components/overkiz/translations/et.json +++ b/homeassistant/components/overkiz/translations/et.json @@ -12,7 +12,6 @@ "too_many_attempts": "Liiga palju katseid kehtetu v\u00f5tmega, ajutiselt keelatud", "too_many_requests": "Liiga palju p\u00e4ringuid, proovi hiljem uuesti", "unknown": "Ootamatu t\u00f5rge", - "unknown_user": "Tundmatu kasutaja. See sidumine ei toeta Somfy Protecti kontosid.", "unsupported_hardware": "See sidumine ei toeta {unsupported_device} riistvara." }, "flow_title": "L\u00fc\u00fcs: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 0fd17d822f5..82997fbd1ae 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -12,7 +12,6 @@ "too_many_attempts": "Trop de tentatives avec un jeton non valide\u00a0: banni temporairement", "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", "unknown": "Erreur inattendue", - "unknown_user": "Utilisateur inconnu. Les comptes Somfy Protect ne sont pas pris en charge par cette int\u00e9gration.", "unsupported_hardware": "Votre mat\u00e9riel {unsupported_device} n'est pas pris en charge par cette int\u00e9gration." }, "flow_title": "Passerelle\u00a0: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/hr.json b/homeassistant/components/overkiz/translations/hr.json new file mode 100644 index 00000000000..4dc5421011a --- /dev/null +++ b/homeassistant/components/overkiz/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unsupported_hardware": "Va\u0161 {unsupported_device} hardver nije podr\u017ean ovom integracijom." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/hu.json b/homeassistant/components/overkiz/translations/hu.json index 95e3090add0..85827bcee50 100644 --- a/homeassistant/components/overkiz/translations/hu.json +++ b/homeassistant/components/overkiz/translations/hu.json @@ -12,7 +12,6 @@ "too_many_attempts": "T\u00fal sok pr\u00f3b\u00e1lkoz\u00e1s \u00e9rv\u00e9nytelen tokennel, ideiglenesen kitiltva", "too_many_requests": "T\u00fal sok a k\u00e9r\u00e9s, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "unknown_user": "Ismeretlen felhaszn\u00e1l\u00f3. Ez az integr\u00e1ci\u00f3 nem t\u00e1mogatja a Somfy Protect fi\u00f3kokat.", "unsupported_hardware": "{unsupported_device} hardver\u00e9t ez az integr\u00e1ci\u00f3 nem t\u00e1mogatja." }, "flow_title": "\u00c1tj\u00e1r\u00f3: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/id.json b/homeassistant/components/overkiz/translations/id.json index 8f4b1912366..0fa0e0b22bb 100644 --- a/homeassistant/components/overkiz/translations/id.json +++ b/homeassistant/components/overkiz/translations/id.json @@ -12,7 +12,6 @@ "too_many_attempts": "Terlalu banyak percobaan dengan token yang tidak valid, untuk sementara diblokir", "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", "unknown": "Kesalahan yang tidak diharapkan", - "unknown_user": "Pengguna tidak dikenal. Akun Somfy Protect tidak didukung oleh integrasi ini.", "unsupported_hardware": "Perangkat keras {unsupported_device} Anda tidak didukung oleh integrasi ini." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/it.json b/homeassistant/components/overkiz/translations/it.json index 49cc4b9a567..96a337b84e9 100644 --- a/homeassistant/components/overkiz/translations/it.json +++ b/homeassistant/components/overkiz/translations/it.json @@ -12,7 +12,6 @@ "too_many_attempts": "Troppi tentativi con un token non valido, temporaneamente bandito", "too_many_requests": "Troppe richieste, riprova pi\u00f9 tardi.", "unknown": "Errore imprevisto", - "unknown_user": "Utente sconosciuto. Gli account Somfy Protect non sono supportati da questa integrazione.", "unsupported_hardware": "L'hardware {unsupported_device} non \u00e8 supportato da questa integrazione." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/ja.json b/homeassistant/components/overkiz/translations/ja.json index d2f72355dfd..df1a438d8ca 100644 --- a/homeassistant/components/overkiz/translations/ja.json +++ b/homeassistant/components/overkiz/translations/ja.json @@ -11,8 +11,7 @@ "server_in_maintenance": "\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306e\u305f\u3081\u30b5\u30fc\u30d0\u30fc\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u307e\u3059", "too_many_attempts": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u306b\u3088\u308b\u8a66\u884c\u56de\u6570\u304c\u591a\u3059\u304e\u305f\u305f\u3081\u3001\u4e00\u6642\u7684\u306b\u7981\u6b62\u3055\u308c\u307e\u3057\u305f\u3002", "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", - "unknown_user": "\u4e0d\u660e\u306a\u30e6\u30fc\u30b6\u30fc\u3067\u3059\u3002Somfy Protect\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/no.json b/homeassistant/components/overkiz/translations/no.json index 062cf053fbd..a1c7c03853d 100644 --- a/homeassistant/components/overkiz/translations/no.json +++ b/homeassistant/components/overkiz/translations/no.json @@ -12,7 +12,6 @@ "too_many_attempts": "For mange fors\u00f8k med et ugyldig token, midlertidig utestengt", "too_many_requests": "For mange foresp\u00f8rsler. Pr\u00f8v igjen senere", "unknown": "Uventet feil", - "unknown_user": "Ukjent bruker. Somfy Protect-kontoer st\u00f8ttes ikke av denne integrasjonen.", "unsupported_hardware": "Maskinvaren din for {unsupported_device} st\u00f8ttes ikke av denne integrasjonen." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/pl.json b/homeassistant/components/overkiz/translations/pl.json index 517ea42ac47..87c365c099f 100644 --- a/homeassistant/components/overkiz/translations/pl.json +++ b/homeassistant/components/overkiz/translations/pl.json @@ -12,7 +12,6 @@ "too_many_attempts": "Zbyt wiele pr\u00f3b z nieprawid\u0142owym tokenem, konto tymczasowo zablokowane", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", "unknown": "Nieoczekiwany b\u0142\u0105d", - "unknown_user": "Nieznany u\u017cytkownik. Konta Somfy Protect nie s\u0105 obs\u0142ugiwane przez t\u0119 integracj\u0119.", "unsupported_hardware": "Twoje urz\u0105dzenie {unsupported_device} nie jest wspierane przez t\u0119 integracj\u0119." }, "flow_title": "Bramka: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/pt-BR.json b/homeassistant/components/overkiz/translations/pt-BR.json index 206b8632656..a7e3f6ff206 100644 --- a/homeassistant/components/overkiz/translations/pt-BR.json +++ b/homeassistant/components/overkiz/translations/pt-BR.json @@ -12,7 +12,6 @@ "too_many_attempts": "Muitas tentativas com um token inv\u00e1lido, banido temporariamente", "too_many_requests": "Muitas solicita\u00e7\u00f5es, tente novamente mais tarde", "unknown": "Erro inesperado", - "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o.", "unsupported_hardware": "Seu hardware {unsupported_device} n\u00e3o \u00e9 compat\u00edvel com esta integra\u00e7\u00e3o." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/pt.json b/homeassistant/components/overkiz/translations/pt.json index 5b2dd940959..1e3d9138c84 100644 --- a/homeassistant/components/overkiz/translations/pt.json +++ b/homeassistant/components/overkiz/translations/pt.json @@ -1,8 +1,7 @@ { "config": { "error": { - "unknown": "Erro inesperado", - "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o." + "unknown": "Erro inesperado" }, "step": { "user": { diff --git a/homeassistant/components/overkiz/translations/ru.json b/homeassistant/components/overkiz/translations/ru.json index 128792152dc..67ca42a6947 100644 --- a/homeassistant/components/overkiz/translations/ru.json +++ b/homeassistant/components/overkiz/translations/ru.json @@ -12,7 +12,6 @@ "too_many_attempts": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0441 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0442\u043e\u043a\u0435\u043d\u043e\u043c, \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c. \u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 Somfy Protect.", "unsupported_hardware": "{unsupported_device} \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439." }, "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/sensor.de.json b/homeassistant/components/overkiz/translations/sensor.de.json index 216df8f0cff..ec73c192a95 100644 --- a/homeassistant/components/overkiz/translations/sensor.de.json +++ b/homeassistant/components/overkiz/translations/sensor.de.json @@ -28,7 +28,7 @@ "wind": "Wind" }, "overkiz__sensor_defect": { - "dead": "nicht Erreichbar", + "dead": "Nicht Erreichbar", "low_battery": "Niedriger Batteriestatus", "maintenance_required": "Wartung erforderlich", "no_defect": "Kein Fehler" diff --git a/homeassistant/components/overkiz/translations/sensor.sk.json b/homeassistant/components/overkiz/translations/sensor.sk.json index cfa849319eb..848abd98c72 100644 --- a/homeassistant/components/overkiz/translations/sensor.sk.json +++ b/homeassistant/components/overkiz/translations/sensor.sk.json @@ -1,10 +1,32 @@ { "state": { + "overkiz__battery": { + "full": "Pln\u00e1", + "low": "N\u00edzke", + "normal": "Norm\u00e1lne", + "verylow": "Ve\u013emi n\u00edzke" + }, "overkiz__discrete_rssi_level": { - "good": "Dobr\u00e1" + "good": "Dobr\u00e1", + "low": "N\u00edzke", + "normal": "Norm\u00e1lne", + "verylow": "Ve\u013emi n\u00edzke" }, "overkiz__priority_lock_originator": { + "local_user": "Miestny pou\u017e\u00edvate\u013e", + "rain": "D\u00e1\u017e\u010f", + "temperature": "Teplota", + "timer": "\u010casova\u010d", + "ups": "UPS", + "user": "Pou\u017e\u00edvate\u013e", "wind": "Vietor" + }, + "overkiz__sensor_defect": { + "low_battery": "Slab\u00e1 bat\u00e9ria" + }, + "overkiz__three_way_handle_direction": { + "closed": "Zatvoren\u00e9", + "open": "Otvori\u0165" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sk.json b/homeassistant/components/overkiz/translations/sk.json index 93eb8dcc1b5..2d386b6d45a 100644 --- a/homeassistant/components/overkiz/translations/sk.json +++ b/homeassistant/components/overkiz/translations/sk.json @@ -1,15 +1,25 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "server_in_maintenance": "Server je vypnut\u00fd z d\u00f4vodu \u00fadr\u017eby", + "too_many_attempts": "Pr\u00edli\u0161 ve\u013ea pokusov s neplatn\u00fdm tokenom, do\u010dasne zak\u00e1zan\u00e9", + "too_many_requests": "Pr\u00edli\u0161 ve\u013ea po\u017eiadaviek, sk\u00faste to nesk\u00f4r", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", + "unsupported_hardware": "V\u00e1\u0161 hardv\u00e9r {unsupported_device} nie je podporovan\u00fd touto integr\u00e1ciou." }, + "flow_title": "Br\u00e1na: {gateway_id}", "step": { "user": { "data": { - "password": "Heslo" + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index 2ae1ca66d32..a5eb948b98b 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -12,7 +12,6 @@ "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd", "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare", "unknown": "Ov\u00e4ntat fel", - "unknown_user": "Ok\u00e4nd anv\u00e4ndare. Somfy Protect-konton st\u00f6ds inte av denna integration.", "unsupported_hardware": "Din {unsupported_device} h\u00e5rdvara st\u00f6ds inte av den h\u00e4r integrationen." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index ed82e138673..f3cb5b70d45 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -12,7 +12,6 @@ "too_many_attempts": "Ge\u00e7ersiz anahtarla \u00e7ok fazla deneme, ge\u00e7ici olarak yasakland\u0131", "too_many_requests": "\u00c7ok fazla istek var, daha sonra tekrar deneyin", "unknown": "Beklenmeyen hata", - "unknown_user": "Bilinmeyen kullan\u0131c\u0131. Somfy Protect hesaplar\u0131 bu entegrasyon taraf\u0131ndan desteklenmez.", "unsupported_hardware": "{unsupported_device} donan\u0131m\u0131n\u0131z bu entegrasyon taraf\u0131ndan desteklenmiyor." }, "flow_title": "A\u011f ge\u00e7idi: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/zh-Hant.json b/homeassistant/components/overkiz/translations/zh-Hant.json index ea8ebcd29dc..072233f23bc 100644 --- a/homeassistant/components/overkiz/translations/zh-Hant.json +++ b/homeassistant/components/overkiz/translations/zh-Hant.json @@ -12,7 +12,6 @@ "too_many_attempts": "\u4f7f\u7528\u7121\u6548\u6b0a\u6756\u5617\u8a66\u6b21\u6578\u904e\u591a\uff0c\u66ab\u6642\u906d\u5230\u5c01\u9396", "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "unknown_user": "\u672a\u77e5\u4f7f\u7528\u8005\u3001\u6b64\u6574\u5408\u4e0d\u652f\u63f4 Somfy Protect \u5e33\u865f\u3002", "unsupported_hardware": "\u6b64\u6574\u5408\u4e0d\u652f\u63f4\u60a8\u7684 {unsupported_device} \u786c\u9ad4\u3002" }, "flow_title": "\u9598\u9053\u5668\uff1a{gateway_id}", diff --git a/homeassistant/components/overkiz/water_heater.py b/homeassistant/components/overkiz/water_heater.py new file mode 100644 index 00000000000..e22f442c266 --- /dev/null +++ b/homeassistant/components/overkiz/water_heater.py @@ -0,0 +1,28 @@ +"""Support for Overkiz water heater devices.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN +from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Overkiz DHW from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( + device.device_url, data.coordinator + ) + for device in data.platforms[Platform.WATER_HEATER] + if device.widget in WIDGET_TO_WATER_HEATER_ENTITY + ) diff --git a/homeassistant/components/overkiz/water_heater_entities/__init__.py b/homeassistant/components/overkiz/water_heater_entities/__init__.py new file mode 100644 index 00000000000..71b66f6ea93 --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/__init__.py @@ -0,0 +1,12 @@ +"""Water heater entities for the Overkiz (by Somfy) integration.""" +from pyoverkiz.enums.ui import UIWidget + +from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW +from .domestic_hot_water_production import DomesticHotWaterProduction +from .hitachi_dhw import HitachiDHW + +WIDGET_TO_WATER_HEATER_ENTITY = { + UIWidget.ATLANTIC_PASS_APC_DHW: AtlanticPassAPCDHW, + UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction, + UIWidget.HITACHI_DHW: HitachiDHW, +} diff --git a/homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py b/homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py new file mode 100644 index 00000000000..7c2ea6ff2d8 --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py @@ -0,0 +1,145 @@ +"""Support for Atlantic Pass APC DHW.""" + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_HEAT_PUMP, + STATE_OFF, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..entity import OverkizEntity + + +class AtlanticPassAPCDHW(OverkizEntity, WaterHeaterEntity): + """Representation of Atlantic Pass APC DHW.""" + + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + ) + _attr_operation_list = [STATE_OFF, STATE_HEAT_PUMP, STATE_PERFORMANCE] + + @property + def target_temperature(self) -> float: + """Return the temperature corresponding to the PRESET.""" + if self.is_boost_mode_on: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_COMFORT_TARGET_DWH_TEMPERATURE + ), + ) + + if self.is_eco_mode_on: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_ECO_TARGET_DWH_TEMPERATURE + ), + ) + + return cast( + float, + self.executor.select_state(OverkizState.CORE_TARGET_DWH_TEMPERATURE), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + + if self.is_eco_mode_on: + await self.executor.async_execute_command( + OverkizCommand.SET_ECO_TARGET_DHW_TEMPERATURE, temperature + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_ECO_TARGET_DWH_TEMPERATURE + ) + else: + await self.executor.async_execute_command( + OverkizCommand.SET_COMFORT_TARGET_DHW_TEMPERATURE, temperature + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_COMFORT_TARGET_DWH_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_DWH_TEMPERATURE + ) + + @property + def is_boost_mode_on(self) -> bool: + """Return true if boost mode is on.""" + return ( + self.executor.select_state(OverkizState.CORE_BOOST_ON_OFF) + == OverkizCommandParam.ON + ) + + @property + def is_eco_mode_on(self) -> bool: + """Return true if eco mode is on.""" + return ( + self.executor.select_state(OverkizState.IO_PASS_APCDWH_MODE) + == OverkizCommandParam.ECO + ) + + @property + def is_away_mode_on(self) -> bool: + """Return true if away mode is on.""" + return ( + self.executor.select_state(OverkizState.CORE_DWH_ON_OFF) + == OverkizCommandParam.OFF + ) + + @property + def current_operation(self) -> str: + """Return current operation.""" + if self.is_boost_mode_on: + return STATE_PERFORMANCE + if self.is_eco_mode_on: + return STATE_ECO + if self.is_away_mode_on: + return STATE_OFF + return STATE_HEAT_PUMP + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + boost_state = OverkizCommandParam.OFF + regular_state = OverkizCommandParam.OFF + if operation_mode == STATE_PERFORMANCE: + boost_state = OverkizCommandParam.ON + regular_state = OverkizCommandParam.ON + elif operation_mode == STATE_HEAT_PUMP: + regular_state = OverkizCommandParam.ON + + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_ON_OFF_STATE, boost_state + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_ON_OFF_STATE, regular_state + ) + + async def async_turn_away_mode_on(self) -> None: + """Turn away mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_ON_OFF_STATE, OverkizCommandParam.OFF + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_ON_OFF_STATE, OverkizCommandParam.OFF + ) + + async def async_turn_away_mode_off(self) -> None: + """Turn away mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_ON_OFF_STATE, OverkizCommandParam.OFF + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_ON_OFF_STATE, OverkizCommandParam.ON + ) diff --git a/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py b/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py new file mode 100644 index 00000000000..49524e19373 --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py @@ -0,0 +1,335 @@ +"""Support for DomesticHotWaterProduction.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +OVERKIZ_TO_OPERATION_MODE: dict[str, str] = { + OverkizCommandParam.STANDARD: STATE_ON, + OverkizCommandParam.HIGH_DEMAND: STATE_HIGH_DEMAND, + OverkizCommandParam.STOP: STATE_OFF, + OverkizCommandParam.MANUAL_ECO_ACTIVE: STATE_ECO, + OverkizCommandParam.MANUAL_ECO_INACTIVE: STATE_OFF, + OverkizCommandParam.ECO: STATE_ECO, + OverkizCommandParam.AUTO: STATE_ECO, + OverkizCommandParam.AUTO_MODE: STATE_ECO, + OverkizCommandParam.BOOST: STATE_PERFORMANCE, +} + +OPERATION_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_OPERATION_MODE.items()} + +DHWP_AWAY_MODES = [ + OverkizCommandParam.ABSENCE, + OverkizCommandParam.AWAY, + OverkizCommandParam.FROSTPROTECTION, +] + +DEFAULT_MIN_TEMP: float = 30 +DEFAULT_MAX_TEMP: float = 70 + + +class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity): + """Representation of a DomesticHotWaterProduction Water Heater.""" + + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + ) + _attr_operation_list = [*OPERATION_MODE_TO_OVERKIZ] + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + + # Init operation mode to set for this specific device + self.overkiz_to_operation_mode: dict[str, str] = {} + state_mode_definition = self.executor.select_definition_state( + OverkizState.IO_DHW_MODE, OverkizState.MODBUSLINK_DHW_MODE + ) + if state_mode_definition and state_mode_definition.values: + # Filter only for mode allowed by this device + for param, mode in OVERKIZ_TO_OPERATION_MODE.items(): + if param in state_mode_definition.values: + self.overkiz_to_operation_mode[param] = mode + else: + self.overkiz_to_operation_mode = OVERKIZ_TO_OPERATION_MODE + + @property + def _is_boost_mode_on(self) -> bool: + """Return true if boost mode is on.""" + + if self.executor.has_state(OverkizState.IO_DHW_BOOST_MODE): + return ( + self.executor.select_state(OverkizState.IO_DHW_BOOST_MODE) + == OverkizCommandParam.ON + ) + + if self.executor.has_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE): + return ( + self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) + == OverkizCommandParam.ON + ) + + if self.executor.has_state(OverkizState.CORE_BOOST_MODE_DURATION): + return ( + cast( + float, + self.executor.select_state(OverkizState.CORE_BOOST_MODE_DURATION), + ) + > 0 + ) + + operating_mode = self.executor.select_state(OverkizState.CORE_OPERATING_MODE) + + if operating_mode: + if isinstance(operating_mode, dict): + if operating_mode.get(OverkizCommandParam.RELAUNCH): + return ( + cast( + str, + operating_mode.get(OverkizCommandParam.RELAUNCH), + ) + == OverkizCommandParam.ON + ) + return False + + return cast(str, operating_mode) == OverkizCommandParam.BOOST + + return False + + @property + def is_away_mode_on(self) -> bool | None: + """Return true if away mode is on.""" + + if self.executor.has_state(OverkizState.IO_DHW_ABSENCE_MODE): + return ( + self.executor.select_state(OverkizState.IO_DHW_ABSENCE_MODE) + == OverkizCommandParam.ON + ) + + if self.executor.has_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE): + return ( + self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE) + == OverkizCommandParam.ON + ) + + operating_mode = self.executor.select_state(OverkizState.CORE_OPERATING_MODE) + + if operating_mode: + if isinstance(operating_mode, dict): + if operating_mode.get(OverkizCommandParam.ABSENCE): + return ( + cast( + str, + operating_mode.get(OverkizCommandParam.ABSENCE), + ) + == OverkizCommandParam.ON + ) + if operating_mode.get(OverkizCommandParam.AWAY): + return ( + cast( + str, + operating_mode.get(OverkizCommandParam.AWAY), + ) + == OverkizCommandParam.ON + ) + return False + + return cast(str, operating_mode) in DHWP_AWAY_MODES + + return None + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + min_temp = self.device.states[OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE] + if min_temp: + return cast(float, min_temp.value_as_float) + return DEFAULT_MIN_TEMP + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + max_temp = self.device.states[OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE] + if max_temp: + return cast(float, max_temp.value_as_float) + return DEFAULT_MAX_TEMP + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + current_temperature = self.device.states[ + OverkizState.IO_MIDDLE_WATER_TEMPERATURE + ] + if current_temperature: + return current_temperature.value_as_float + current_temperature = self.device.states[ + OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE + ] + if current_temperature: + return current_temperature.value_as_float + return None + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + + target_temperature = self.device.states[ + OverkizState.CORE_WATER_TARGET_TEMPERATURE + ] + if target_temperature: + return target_temperature.value_as_float + + target_temperature = self.device.states[ + OverkizState.CORE_TARGET_DWH_TEMPERATURE + ] + if target_temperature: + return target_temperature.value_as_float + + target_temperature = self.device.states[OverkizState.CORE_TARGET_TEMPERATURE] + if target_temperature: + return target_temperature.value_as_float + + return None + + @property + def target_temperature_high(self) -> float | None: + """Return the highbound target temperature we try to reach.""" + target_temperature_high = self.device.states[ + OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE + ] + if target_temperature_high: + return target_temperature_high.value_as_float + return None + + @property + def target_temperature_low(self) -> float | None: + """Return the lowbound target temperature we try to reach.""" + target_temperature_low = self.device.states[ + OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE + ] + if target_temperature_low: + return target_temperature_low.value_as_float + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + target_temperature = kwargs.get(ATTR_TEMPERATURE) + + if self.executor.has_command(OverkizCommand.SET_TARGET_TEMPERATURE): + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_TEMPERATURE, target_temperature + ) + elif self.executor.has_command(OverkizCommand.SET_WATER_TARGET_TEMPERATURE): + await self.executor.async_execute_command( + OverkizCommand.SET_WATER_TARGET_TEMPERATURE, target_temperature + ) + + if self.executor.has_command(OverkizCommand.REFRESH_TARGET_TEMPERATURE): + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + elif self.executor.has_command(OverkizCommand.REFRESH_WATER_TARGET_TEMPERATURE): + await self.executor.async_execute_command( + OverkizCommand.REFRESH_WATER_TARGET_TEMPERATURE + ) + + @property + def current_operation(self) -> str: + """Return current operation ie. eco, electric, performance, ...""" + if self._is_boost_mode_on: + return OVERKIZ_TO_OPERATION_MODE[OverkizCommandParam.BOOST] + + return OVERKIZ_TO_OPERATION_MODE[ + cast( + str, + self.executor.select_state( + OverkizState.IO_DHW_MODE, OverkizState.MODBUSLINK_DHW_MODE + ), + ) + ] + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new target operation mode.""" + + if operation_mode == STATE_PERFORMANCE: + if self.executor.has_command(OverkizCommand.SET_BOOST_MODE): + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommand.ON + ) + + if self.executor.has_command(OverkizCommand.SET_BOOST_MODE_DURATION): + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE_DURATION, 7 + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_BOOST_MODE_DURATION + ) + + if self.executor.has_command(OverkizCommand.SET_CURRENT_OPERATING_MODE): + current_operating_mode = self.executor.select_state( + OverkizState.CORE_OPERATING_MODE + ) + + if current_operating_mode and isinstance(current_operating_mode, dict): + await self.executor.async_execute_command( + OverkizCommand.SET_CURRENT_OPERATING_MODE, + { + OverkizCommandParam.RELAUNCH: OverkizCommandParam.ON, + OverkizCommandParam.ABSENCE: OverkizCommandParam.OFF, + }, + ) + + return + + if self._is_boost_mode_on: + # We're setting a non Boost mode and the device is currently in Boost mode, the following code remove all boost operations + if self.executor.has_command(OverkizCommand.SET_BOOST_MODE): + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE, OverkizCommand.OFF + ) + + if self.executor.has_command(OverkizCommand.SET_BOOST_MODE_DURATION): + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_MODE_DURATION, 0 + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_BOOST_MODE_DURATION + ) + + if self.executor.has_command(OverkizCommand.SET_CURRENT_OPERATING_MODE): + current_operating_mode = self.executor.select_state( + OverkizState.CORE_OPERATING_MODE + ) + + if current_operating_mode and isinstance(current_operating_mode, dict): + await self.executor.async_execute_command( + OverkizCommand.SET_CURRENT_OPERATING_MODE, + { + OverkizCommandParam.RELAUNCH: OverkizCommandParam.OFF, + OverkizCommandParam.ABSENCE: OverkizCommandParam.OFF, + }, + ) + + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, self.overkiz_to_operation_mode[operation_mode] + ) + + if self.executor.has_command(OverkizCommand.REFRESH_DHW_MODE): + await self.executor.async_execute_command(OverkizCommand.REFRESH_DHW_MODE) diff --git a/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py b/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py new file mode 100644 index 00000000000..fa8c128e0c1 --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py @@ -0,0 +1,101 @@ +"""Support for Hitachi DHW.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_HIGH_DEMAND, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_WHOLE, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, +) + +from ..entity import OverkizEntity + +OVERKIZ_TO_OPERATION_MODE: dict[str, str] = { + OverkizCommandParam.STANDARD: STATE_ON, + OverkizCommandParam.HIGH_DEMAND: STATE_HIGH_DEMAND, + OverkizCommandParam.STOP: STATE_OFF, +} + +OPERATION_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_OPERATION_MODE.items()} + + +class HitachiDHW(OverkizEntity, WaterHeaterEntity): + """Representation of Hitachi DHW.""" + + _attr_min_temp = 30.0 + _attr_max_temp = 70.0 + _attr_precision = PRECISION_WHOLE + + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + ) + _attr_operation_list = [*OPERATION_MODE_TO_OVERKIZ] + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + current_temperature = self.device.states[OverkizState.CORE_DHW_TEMPERATURE] + if current_temperature: + return current_temperature.value_as_float + return None + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + target_temperature = self.device.states[ + OverkizState.MODBUS_CONTROL_DHW_SETTING_TEMPERATURE + ] + if target_temperature: + return target_temperature.value_as_float + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temperature = cast(float, kwargs.get(ATTR_TEMPERATURE)) + await self.executor.async_execute_command( + OverkizCommand.SET_CONTROL_DHW_SETTING_TEMPERATURE, int(temperature) + ) + + @property + def current_operation(self) -> str | None: + """Return current operation ie. eco, electric, performance, ...""" + modbus_control = self.device.states[OverkizState.MODBUS_CONTROL_DHW] + if modbus_control and modbus_control.value_as_str == OverkizCommandParam.STOP: + return STATE_OFF + + current_mode = self.device.states[OverkizState.MODBUS_DHW_MODE] + if current_mode and current_mode.value_as_str in OVERKIZ_TO_OPERATION_MODE: + return OVERKIZ_TO_OPERATION_MODE[current_mode.value_as_str] + + return None + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new target operation mode.""" + # Turn water heater off + if operation_mode == OverkizCommandParam.OFF: + return await self.executor.async_execute_command( + OverkizCommand.SET_CONTROL_DHW, OverkizCommandParam.STOP + ) + + # Turn water heater on, when off + if self.current_operation == OverkizCommandParam.OFF: + await self.executor.async_execute_command( + OverkizCommand.SET_CONTROL_DHW, OverkizCommandParam.ON + ) + + # Change operation mode + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_MODE, OPERATION_MODE_TO_OVERKIZ[operation_mode] + ) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index cb2ded6fcef..cc19b0454b1 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from .const import CONF_ACCOUNT, DATA_CLIENT, DATA_COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -35,7 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: authenticated = await client.authenticate( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_ACCOUNT], ) except aiohttp.ClientError as exception: _LOGGER.warning(exception) @@ -49,7 +51,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(10): try: authenticated = await client.authenticate( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_ACCOUNT], ) except aiohttp.ClientError as exception: raise UpdateFailed(exception) from exception diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index f4dcc5301d7..9e406ce6b96 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -6,11 +6,15 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN +from .const import CONF_ACCOUNT, DOMAIN REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) USER_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_ACCOUNT): str, + } ) @@ -22,6 +26,7 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the flow.""" self.username = None + self.account = None async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" @@ -30,7 +35,9 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): client = OVOEnergy() try: authenticated = await client.authenticate( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + user_input.get(CONF_ACCOUNT, None), ) except aiohttp.ClientError: errors["base"] = "cannot_connect" @@ -44,6 +51,7 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): data={ CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_ACCOUNT: client.account_id, }, ) @@ -60,13 +68,16 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): if user_input and user_input.get(CONF_USERNAME): self.username = user_input[CONF_USERNAME] + if user_input and user_input.get(CONF_ACCOUNT): + self.account = user_input[CONF_ACCOUNT] + self.context["title_placeholders"] = {CONF_USERNAME: self.username} if user_input is not None and user_input.get(CONF_PASSWORD) is not None: client = OVOEnergy() try: authenticated = await client.authenticate( - self.username, user_input[CONF_PASSWORD] + self.username, user_input[CONF_PASSWORD], self.account ) except aiohttp.ClientError: errors["base"] = "connection_error" @@ -78,6 +89,7 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): data={ CONF_USERNAME: self.username, CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_ACCOUNT: self.account, }, ) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/ovo_energy/const.py b/homeassistant/components/ovo_energy/const.py index f691eb9bc49..1068c5443fd 100644 --- a/homeassistant/components/ovo_energy/const.py +++ b/homeassistant/components/ovo_energy/const.py @@ -3,3 +3,4 @@ DOMAIN = "ovo_energy" DATA_CLIENT = "ovo_client" DATA_COORDINATOR = "coordinator" +CONF_ACCOUNT = "account" diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 6d994e472c8..e61aaebe190 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/ovo_energy", "requirements": ["ovoenergy==1.2.0"], "codeowners": ["@timmo001"], + "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["ovoenergy"] } diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index 69392295899..810602b1412 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -10,7 +10,8 @@ "user": { "data": { "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "account": "OVO account id (only add if you have multiple accounts)" }, "description": "Set up an OVO Energy instance to access your energy usage.", "title": "Add OVO Energy Account" diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json index b0c9e8a77cc..7207f59fef2 100644 --- a/homeassistant/components/ovo_energy/translations/bg.json +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -11,7 +11,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json index 0d0677ec522..87278926984 100644 --- a/homeassistant/components/ovo_energy/translations/ca.json +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "Identificador de compte OVO (afegeix-lo nom\u00e9s si tens diversos comptes)", "password": "Contrasenya", "username": "Nom d'usuari" }, diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index 74d3007430f..bc1a8187a29 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO Konto-ID (nur hinzuf\u00fcgen, wenn mehrere Konten vorhanden sind)", "password": "Passwort", "username": "Benutzername" }, diff --git a/homeassistant/components/ovo_energy/translations/el.json b/homeassistant/components/ovo_energy/translations/el.json index b8b9d35a238..0a0949fd703 100644 --- a/homeassistant/components/ovo_energy/translations/el.json +++ b/homeassistant/components/ovo_energy/translations/el.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd OVO (\u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03bf\u03bb\u03bb\u03bf\u03cd\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd\u03c2)", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 3539d91220d..e3d88a6a85f 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO account id (only add if you have multiple accounts)", "password": "Password", "username": "Username" }, diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index d3ffba9b2e5..a41cff3a535 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID de la cuenta OVO (solo a\u00f1adir si tienes varias cuentas)", "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, diff --git a/homeassistant/components/ovo_energy/translations/et.json b/homeassistant/components/ovo_energy/translations/et.json index ab33d27f337..f7ce2be8a12 100644 --- a/homeassistant/components/ovo_energy/translations/et.json +++ b/homeassistant/components/ovo_energy/translations/et.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO konto ID (lisa ainult siis, kui on mitu kontot)", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index a58669fe11c..5be71f15ac7 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO-fi\u00f3k azonos\u00edt\u00f3 (csak akkor adja meg, ha t\u00f6bb fi\u00f3kkal rendelkezik)", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/ovo_energy/translations/id.json b/homeassistant/components/ovo_energy/translations/id.json index fa072b59236..644dc357d72 100644 --- a/homeassistant/components/ovo_energy/translations/id.json +++ b/homeassistant/components/ovo_energy/translations/id.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID akun OVO (hanya tambahkan jika Anda memiliki beberapa akun)", "password": "Kata Sandi", "username": "Nama Pengguna" }, diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json index be21bde5665..c5069e5da86 100644 --- a/homeassistant/components/ovo_energy/translations/it.json +++ b/homeassistant/components/ovo_energy/translations/it.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID account OVO (aggiungi solo se hai pi\u00f9 account)", "password": "Password", "username": "Nome utente" }, diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 64b989de78b..23473cca581 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO-konto-ID (bare legg til hvis du har flere kontoer)", "password": "Passord", "username": "Brukernavn" }, diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json index c426d140417..a14ae9d2b6b 100644 --- a/homeassistant/components/ovo_energy/translations/pl.json +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "Identyfikator konta OVO (dodaj tylko, je\u015bli masz wiele kont)", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, diff --git a/homeassistant/components/ovo_energy/translations/pt-BR.json b/homeassistant/components/ovo_energy/translations/pt-BR.json index 4b73ecca8b9..7af4adf2389 100644 --- a/homeassistant/components/ovo_energy/translations/pt-BR.json +++ b/homeassistant/components/ovo_energy/translations/pt-BR.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID da conta OVO (adicione apenas se voc\u00ea tiver v\u00e1rias contas)", "password": "Senha", "username": "Usu\u00e1rio" }, diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index f44b0e27110..b1457aeba68 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 OVO (\u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u0432)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, diff --git a/homeassistant/components/ovo_energy/translations/sk.json b/homeassistant/components/ovo_energy/translations/sk.json index 5ada995aa6e..e860c7345ce 100644 --- a/homeassistant/components/ovo_energy/translations/sk.json +++ b/homeassistant/components/ovo_energy/translations/sk.json @@ -1,7 +1,24 @@ { "config": { "error": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "flow_title": "{username}", + "step": { + "reauth": { + "data": { + "password": "Heslo" + }, + "description": "Autentifik\u00e1cia pre OVO Energy zlyhala. Zadajte svoje aktu\u00e1lne poverenia." + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json index 0b1c218d94c..3bc858fd24e 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hant.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO \u5e33\u865f ID\uff08\u7576\u6709\u591a\u500b\u5e33\u865f\u6642\u624d\u9700\u586b\u5beb\uff09", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index b65ac9ccbe7..f983d0f98d4 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,6 +1,10 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" -from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, DOMAIN, SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import ( + ATTR_SOURCE_TYPE, + DOMAIN, + SourceType, + TrackerEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/owntracks/translations/sk.json b/homeassistant/components/owntracks/translations/sk.json new file mode 100644 index 00000000000..c9ae6fa2722 --- /dev/null +++ b/homeassistant/components/owntracks/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Naozaj chcete nastavi\u0165 OwnTracks?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/bg.json b/homeassistant/components/p1_monitor/translations/bg.json index acbebfb4d36..824249b169b 100644 --- a/homeassistant/components/p1_monitor/translations/bg.json +++ b/homeassistant/components/p1_monitor/translations/bg.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u0418\u043c\u0435" + "host": "\u0425\u043e\u0441\u0442" } } } diff --git a/homeassistant/components/p1_monitor/translations/ca.json b/homeassistant/components/p1_monitor/translations/ca.json index 6d65ba16b5c..5c806805c24 100644 --- a/homeassistant/components/p1_monitor/translations/ca.json +++ b/homeassistant/components/p1_monitor/translations/ca.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Amfitri\u00f3", - "name": "Nom" + "host": "Amfitri\u00f3" }, "data_description": { "host": "Adre\u00e7a IP o nom d'amfitri\u00f3 de la instal\u00b7laci\u00f3 P1 Monitor." diff --git a/homeassistant/components/p1_monitor/translations/cs.json b/homeassistant/components/p1_monitor/translations/cs.json index 7a27355056b..44f546c1c29 100644 --- a/homeassistant/components/p1_monitor/translations/cs.json +++ b/homeassistant/components/p1_monitor/translations/cs.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Hostitel", - "name": "Jm\u00e9no" + "host": "Hostitel" } } } diff --git a/homeassistant/components/p1_monitor/translations/de.json b/homeassistant/components/p1_monitor/translations/de.json index 8740c9dccbb..1da6e7da32d 100644 --- a/homeassistant/components/p1_monitor/translations/de.json +++ b/homeassistant/components/p1_monitor/translations/de.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Name" + "host": "Host" }, "data_description": { "host": "Die IP-Adresse oder der Hostname deiner P1 Monitor-Installation." diff --git a/homeassistant/components/p1_monitor/translations/el.json b/homeassistant/components/p1_monitor/translations/el.json index bd30b70ef63..fe6dfaf6fd0 100644 --- a/homeassistant/components/p1_monitor/translations/el.json +++ b/homeassistant/components/p1_monitor/translations/el.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, "data_description": { "host": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c4\u03b7\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03bf\u03b8\u03cc\u03bd\u03b7\u03c2 P1." diff --git a/homeassistant/components/p1_monitor/translations/en.json b/homeassistant/components/p1_monitor/translations/en.json index 4347e2d89d2..394b6c0767b 100644 --- a/homeassistant/components/p1_monitor/translations/en.json +++ b/homeassistant/components/p1_monitor/translations/en.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Name" + "host": "Host" }, "data_description": { "host": "The IP address or hostname of your P1 Monitor installation." diff --git a/homeassistant/components/p1_monitor/translations/es.json b/homeassistant/components/p1_monitor/translations/es.json index 3893952a1ec..4ed069e96e4 100644 --- a/homeassistant/components/p1_monitor/translations/es.json +++ b/homeassistant/components/p1_monitor/translations/es.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nombre" + "host": "Host" }, "data_description": { "host": "La direcci\u00f3n IP o el nombre de host de tu instalaci\u00f3n de P1 Monitor." diff --git a/homeassistant/components/p1_monitor/translations/et.json b/homeassistant/components/p1_monitor/translations/et.json index 18a561ea30b..e3e21aa3ef7 100644 --- a/homeassistant/components/p1_monitor/translations/et.json +++ b/homeassistant/components/p1_monitor/translations/et.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nimi" + "host": "Host" }, "data_description": { "host": "P1 Monitori paigalduse IP-aadress v\u00f5i hostinimi." diff --git a/homeassistant/components/p1_monitor/translations/fr.json b/homeassistant/components/p1_monitor/translations/fr.json index 5da5fe439cd..22e683fbf44 100644 --- a/homeassistant/components/p1_monitor/translations/fr.json +++ b/homeassistant/components/p1_monitor/translations/fr.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te", - "name": "Nom" + "host": "H\u00f4te" }, "data_description": { "host": "L'adresse IP ou le nom d'h\u00f4te de votre installation P1 Monitor." diff --git a/homeassistant/components/p1_monitor/translations/he.json b/homeassistant/components/p1_monitor/translations/he.json index 33660936e12..ab620ca81eb 100644 --- a/homeassistant/components/p1_monitor/translations/he.json +++ b/homeassistant/components/p1_monitor/translations/he.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "name": "\u05e9\u05dd" + "host": "\u05de\u05d0\u05e8\u05d7" } } } diff --git a/homeassistant/components/p1_monitor/translations/hu.json b/homeassistant/components/p1_monitor/translations/hu.json index 0b6071be83f..bac2951ba2f 100644 --- a/homeassistant/components/p1_monitor/translations/hu.json +++ b/homeassistant/components/p1_monitor/translations/hu.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "C\u00edm", - "name": "Elnevez\u00e9s" + "host": "C\u00edm" }, "data_description": { "host": "A P1 Monitor rendszer\u00e9nek IP-c\u00edme vagy hostneve." diff --git a/homeassistant/components/p1_monitor/translations/id.json b/homeassistant/components/p1_monitor/translations/id.json index 52bb67d00a6..ce5860ec401 100644 --- a/homeassistant/components/p1_monitor/translations/id.json +++ b/homeassistant/components/p1_monitor/translations/id.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nama" + "host": "Host" }, "data_description": { "host": "Alamat IP atau nama host instalasi P1 Monitor Anda." diff --git a/homeassistant/components/p1_monitor/translations/it.json b/homeassistant/components/p1_monitor/translations/it.json index ae529d7cb92..e0c54b5d426 100644 --- a/homeassistant/components/p1_monitor/translations/it.json +++ b/homeassistant/components/p1_monitor/translations/it.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nome" + "host": "Host" }, "data_description": { "host": "L'indirizzo IP o il nome host dell'installazione di P1 Monitor." diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json index 66e79ebdc0b..765e8930f5f 100644 --- a/homeassistant/components/p1_monitor/translations/ja.json +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8", - "name": "\u540d\u524d" + "host": "\u30db\u30b9\u30c8" }, "data_description": { "host": "P1 Monitor\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u3001IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30db\u30b9\u30c8\u540d\u3002" diff --git a/homeassistant/components/p1_monitor/translations/nl.json b/homeassistant/components/p1_monitor/translations/nl.json index 1f5e9e5a422..614a0079589 100644 --- a/homeassistant/components/p1_monitor/translations/nl.json +++ b/homeassistant/components/p1_monitor/translations/nl.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Naam" + "host": "Host" }, "description": "Stel P1 Monitor in om te integreren met Home Assistant." } diff --git a/homeassistant/components/p1_monitor/translations/no.json b/homeassistant/components/p1_monitor/translations/no.json index 16e58ba7c7b..4aa965a7bf9 100644 --- a/homeassistant/components/p1_monitor/translations/no.json +++ b/homeassistant/components/p1_monitor/translations/no.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Vert", - "name": "Navn" + "host": "Vert" }, "data_description": { "host": "IP-adressen eller vertsnavnet til P1 Monitor-installasjonen." diff --git a/homeassistant/components/p1_monitor/translations/pl.json b/homeassistant/components/p1_monitor/translations/pl.json index 93e2b7a83bd..d935010937d 100644 --- a/homeassistant/components/p1_monitor/translations/pl.json +++ b/homeassistant/components/p1_monitor/translations/pl.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "name": "Nazwa" + "host": "Nazwa hosta lub adres IP" }, "data_description": { "host": "Adres IP lub nazwa hosta instalacji monitora P1." diff --git a/homeassistant/components/p1_monitor/translations/pt-BR.json b/homeassistant/components/p1_monitor/translations/pt-BR.json index 97dfd9a0ec3..b49bbab0c36 100644 --- a/homeassistant/components/p1_monitor/translations/pt-BR.json +++ b/homeassistant/components/p1_monitor/translations/pt-BR.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Nome do host", - "name": "Nome" + "host": "Nome do host" }, "data_description": { "host": "O endere\u00e7o IP ou o nome do host da instala\u00e7\u00e3o do P1 Monitor." diff --git a/homeassistant/components/p1_monitor/translations/pt.json b/homeassistant/components/p1_monitor/translations/pt.json index ab627843537..602d9c6d009 100644 --- a/homeassistant/components/p1_monitor/translations/pt.json +++ b/homeassistant/components/p1_monitor/translations/pt.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Servidor", - "name": "Nome" + "host": "Servidor" }, "data_description": { "host": "O endere\u00e7o IP ou nome de host da instala\u00e7\u00e3o do Monitor P1." diff --git a/homeassistant/components/p1_monitor/translations/ru.json b/homeassistant/components/p1_monitor/translations/ru.json index 75a179abdc3..f5873b76db7 100644 --- a/homeassistant/components/p1_monitor/translations/ru.json +++ b/homeassistant/components/p1_monitor/translations/ru.json @@ -6,11 +6,10 @@ "step": { "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + "host": "\u0425\u043e\u0441\u0442" }, "data_description": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 P1 Monitor." + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 P1 Monitor." }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 P1 Monitor." } diff --git a/homeassistant/components/p1_monitor/translations/sk.json b/homeassistant/components/p1_monitor/translations/sk.json index af15f92c2f2..d7ec1bab0b6 100644 --- a/homeassistant/components/p1_monitor/translations/sk.json +++ b/homeassistant/components/p1_monitor/translations/sk.json @@ -1,10 +1,17 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { - "name": "N\u00e1zov" - } + "host": "Hostite\u013e" + }, + "data_description": { + "host": "IP adresa alebo n\u00e1zov hostite\u013ea va\u0161ej in\u0161tal\u00e1cie monitora P1." + }, + "description": "Nastavte P1 Monitor na integr\u00e1ciu s Home Assistant." } } } diff --git a/homeassistant/components/p1_monitor/translations/sv.json b/homeassistant/components/p1_monitor/translations/sv.json index 2a4c3c62277..fbce408ee45 100644 --- a/homeassistant/components/p1_monitor/translations/sv.json +++ b/homeassistant/components/p1_monitor/translations/sv.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "V\u00e4rd", - "name": "Namn" + "host": "V\u00e4rd" }, "data_description": { "host": "IP-adressen eller v\u00e4rdnamnet f\u00f6r din P1 Monitor-installation." diff --git a/homeassistant/components/p1_monitor/translations/tr.json b/homeassistant/components/p1_monitor/translations/tr.json index 1ee8c351c8d..80dce588bc7 100644 --- a/homeassistant/components/p1_monitor/translations/tr.json +++ b/homeassistant/components/p1_monitor/translations/tr.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "Sunucu", - "name": "Ad" + "host": "Sunucu" }, "data_description": { "host": "P1 Monitor kurulumunuzun IP adresi veya ana bilgisayar ad\u0131." diff --git a/homeassistant/components/p1_monitor/translations/zh-Hant.json b/homeassistant/components/p1_monitor/translations/zh-Hant.json index 27e0200f1b2..0d899fb74b6 100644 --- a/homeassistant/components/p1_monitor/translations/zh-Hant.json +++ b/homeassistant/components/p1_monitor/translations/zh-Hant.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "host": "\u4e3b\u6a5f\u7aef", - "name": "\u540d\u7a31" + "host": "\u4e3b\u6a5f\u7aef" }, "data_description": { "host": "P1 Monitor \u5b89\u88dd IP \u4f4d\u5740\u6216\u4e3b\u6a5f\u540d\u7a31\u3002" diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index 54062b36b1b..1e04d5ce53d 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -48,6 +48,7 @@ def setup_platform( class PanasonicBluRay(MediaPlayerEntity): """Representation of a Panasonic Blu-ray device.""" + _attr_icon = "mdi:disc-player" _attr_supported_features = ( MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF @@ -59,41 +60,10 @@ class PanasonicBluRay(MediaPlayerEntity): def __init__(self, ip, name): """Initialize the Panasonic Blue-ray device.""" self._device = PanasonicBD(ip) - self._name = name - self._state = MediaPlayerState.OFF - self._position = 0 - self._duration = 0 - self._position_valid = 0 - - @property - def icon(self): - """Return a disc player icon for the device.""" - return "mdi:disc-player" - - @property - def name(self): - """Return the display name of this device.""" - return self._name - - @property - def state(self): - """Return _state variable, containing the appropriate constant.""" - return self._state - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - return self._duration - - @property - def media_position(self): - """Position of current playing media in seconds.""" - return self._position - - @property - def media_position_updated_at(self): - """When was the position of the current playing media valid.""" - return self._position_valid + self._attr_name = name + self._attr_state = MediaPlayerState.OFF + self._attr_media_position = 0 + self._attr_media_duration = 0 def update(self) -> None: """Update the internal state by querying the device.""" @@ -101,24 +71,24 @@ class PanasonicBluRay(MediaPlayerEntity): state = self._device.get_play_status() if state[0] == "error": - self._state = None + self._attr_state = None elif state[0] in ["off", "standby"]: # We map both of these to off. If it's really off we can't # turn it on, but from standby we can go to idle by pressing # POWER. - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF elif state[0] in ["paused", "stopped"]: - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE elif state[0] == "playing": - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING # Update our current media position + length if state[1] >= 0: - self._position = state[1] + self._attr_media_position = state[1] else: - self._position = 0 - self._position_valid = utcnow() - self._duration = state[2] + self._attr_media_position = 0 + self._attr_media_position_updated_at = utcnow() + self._attr_media_duration = state[2] def turn_off(self) -> None: """ @@ -129,17 +99,17 @@ class PanasonicBluRay(MediaPlayerEntity): our favour as it means the device is still accepting commands and we can thus turn it back on when desired. """ - if self._state != MediaPlayerState.OFF: + if self.state != MediaPlayerState.OFF: self._device.send_key("POWER") - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF def turn_on(self) -> None: """Wake the device back up from standby.""" - if self._state == MediaPlayerState.OFF: + if self.state == MediaPlayerState.OFF: self._device.send_key("POWER") - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE def media_play(self) -> None: """Send play command.""" diff --git a/homeassistant/components/panasonic_viera/translations/bg.json b/homeassistant/components/panasonic_viera/translations/bg.json index 2ceff63752c..c8dab4b29d9 100644 --- a/homeassistant/components/panasonic_viera/translations/bg.json +++ b/homeassistant/components/panasonic_viera/translations/bg.json @@ -6,6 +6,7 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_pin_code": "\u0412\u044a\u0432\u0435\u0434\u0435\u043d\u0438\u044f\u0442 \u043e\u0442 \u0412\u0430\u0441 \u041f\u0418\u041d \u043a\u043e\u0434 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d" }, "step": { diff --git a/homeassistant/components/panasonic_viera/translations/sk.json b/homeassistant/components/panasonic_viera/translations/sk.json index af15f92c2f2..b4bdedf9793 100644 --- a/homeassistant/components/panasonic_viera/translations/sk.json +++ b/homeassistant/components/panasonic_viera/translations/sk.json @@ -1,10 +1,29 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_pin_code": "Zadan\u00fd PIN k\u00f3d bol neplatn\u00fd" + }, "step": { + "pairing": { + "data": { + "pin": "PIN k\u00f3d" + }, + "description": "Zadajte PIN k\u00f3d zobrazen\u00fd na va\u0161om telev\u00edzore", + "title": "P\u00e1rovanie" + }, "user": { "data": { + "host": "IP adresa", "name": "N\u00e1zov" - } + }, + "description": "Zadajte IP adresa v\u00e1\u0161ho telev\u00edzora Panasonic Viera TV", + "title": "Nastavenie telev\u00edzora" } } } diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index c45c04a330d..b728767cb4e 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -81,30 +81,20 @@ class PandoraMediaPlayer(MediaPlayerEntity): def __init__(self, name): """Initialize the Pandora device.""" - self._name = name - self._player_state = MediaPlayerState.OFF - self._station = "" - self._media_title = "" - self._media_artist = "" - self._media_album = "" - self._stations = [] + self._attr_name = name + self._attr_state = MediaPlayerState.OFF + self._attr_source = "" + self._attr_media_title = "" + self._attr_media_artist = "" + self._attr_media_album_name = "" + self._attr_source_list = [] self._time_remaining = 0 - self._media_duration = 0 + self._attr_media_duration = 0 self._pianobar = None - @property - def name(self): - """Return the name of the media player.""" - return self._name - - @property - def state(self): - """Return the state of the player.""" - return self._player_state - def turn_on(self) -> None: """Turn the media player on.""" - if self._player_state != MediaPlayerState.OFF: + if self.state != MediaPlayerState.OFF: return self._pianobar = pexpect.spawn("pianobar") _LOGGER.info("Started pianobar subprocess") @@ -129,7 +119,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._update_stations() self.update_playing_status() - self._player_state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE self.schedule_update_ha_state() def turn_off(self) -> None: @@ -146,19 +136,19 @@ class PandoraMediaPlayer(MediaPlayerEntity): os.killpg(os.getpgid(self._pianobar.pid), signal.SIGTERM) _LOGGER.debug("Killed Pianobar subprocess") self._pianobar = None - self._player_state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() def media_play(self) -> None: """Send play command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) - self._player_state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def media_pause(self) -> None: """Send pause command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) - self._player_state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED self.schedule_update_ha_state() def media_next_track(self) -> None: @@ -167,40 +157,17 @@ class PandoraMediaPlayer(MediaPlayerEntity): self.schedule_update_ha_state() @property - def source(self): - """Name of the current input source.""" - return self._station - - @property - def source_list(self): - """List of available input sources.""" - return self._stations - - @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" self.update_playing_status() - return self._media_title - - @property - def media_artist(self): - """Artist of current playing media, music track only.""" - return self._media_artist - - @property - def media_album_name(self): - """Album name of current playing media, music track only.""" - return self._media_album - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - return self._media_duration + return self._attr_media_title def select_source(self, source: str) -> None: """Choose a different Pandora station and play it.""" + if self.source_list is None: + return try: - station_index = self._stations.index(source) + station_index = self.source_list.index(source) except ValueError: _LOGGER.warning("Station %s is not in list", source) return @@ -208,7 +175,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._send_station_list_command() self._pianobar.sendline(f"{station_index}") self._pianobar.expect("\r\n") - self._player_state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def _send_station_list_command(self): """Send a station list command.""" @@ -269,8 +236,8 @@ class PandoraMediaPlayer(MediaPlayerEntity): def _update_current_station(self, response): """Update current station.""" if station_match := re.search(STATION_PATTERN, response): - self._station = station_match.group(1) - _LOGGER.debug("Got station as: %s", self._station) + self._attr_source = station_match.group(1) + _LOGGER.debug("Got station as: %s", self._attr_source) else: _LOGGER.warning("No station match") @@ -278,11 +245,11 @@ class PandoraMediaPlayer(MediaPlayerEntity): """Update info about current song.""" if song_match := re.search(CURRENT_SONG_PATTERN, response): ( - self._media_title, - self._media_artist, - self._media_album, + self._attr_media_title, + self._attr_media_artist, + self._attr_media_album_name, ) = song_match.groups() - _LOGGER.debug("Got song as: %s", self._media_title) + _LOGGER.debug("Got song as: %s", self._attr_media_title) else: _LOGGER.warning("No song match") @@ -302,12 +269,12 @@ class PandoraMediaPlayer(MediaPlayerEntity): total_seconds, ) = self._pianobar.match.groups() time_remaining = int(cur_minutes) * 60 + int(cur_seconds) - self._media_duration = int(total_minutes) * 60 + int(total_seconds) + self._attr_media_duration = int(total_minutes) * 60 + int(total_seconds) - if time_remaining not in (self._time_remaining, self._media_duration): - self._player_state = MediaPlayerState.PLAYING - elif self._player_state == MediaPlayerState.PLAYING: - self._player_state = MediaPlayerState.PAUSED + if time_remaining not in (self._time_remaining, self._attr_media_duration): + self._attr_state = MediaPlayerState.PLAYING + elif self.state == MediaPlayerState.PLAYING: + self._attr_state = MediaPlayerState.PAUSED self._time_remaining = time_remaining def _log_match(self): @@ -333,12 +300,12 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._send_station_list_command() station_lines = self._pianobar.before.decode("utf-8") _LOGGER.debug("Getting stations: %s", station_lines) - self._stations = [] + self._attr_source_list = [] for line in station_lines.split("\r\n"): if match := re.search(r"\d+\).....(.+)", line): station = match.group(1).strip() _LOGGER.debug("Found station %s", station) - self._stations.append(station) + self._attr_source_list.append(station) else: _LOGGER.debug("No station match on %s", line) self._pianobar.sendcontrol("m") # press enter with blank line diff --git a/homeassistant/components/peco/translations/sk.json b/homeassistant/components/peco/translations/sk.json new file mode 100644 index 00000000000..09453d63993 --- /dev/null +++ b/homeassistant/components/peco/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "user": { + "data": { + "county": "Okres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index a31212be3f7..3287d907578 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -2,10 +2,9 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Mapping +from collections.abc import Mapping from datetime import timedelta import logging -from typing import Any from haphilipsjs import AutenticationFailure, ConnectionFailure, PhilipsTV from haphilipsjs.typing import SystemType @@ -18,9 +17,8 @@ from homeassistant.const import ( CONF_USERNAME, Platform, ) -from homeassistant.core import Context, HassJob, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.trigger import TriggerActionType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN @@ -78,42 +76,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class PluggableAction: - """A pluggable action handler.""" - - def __init__(self, update: Callable[[], None]) -> None: - """Initialize.""" - self._update = update - self._actions: dict[ - Any, tuple[HassJob[..., Coroutine[Any, Any, None]], dict[str, Any]] - ] = {} - - def __bool__(self): - """Return if we have something attached.""" - return bool(self._actions) - - @callback - def async_attach(self, action: TriggerActionType, variables: dict[str, Any]): - """Attach a device trigger for turn on.""" - - @callback - def _remove(): - del self._actions[_remove] - self._update() - - job = HassJob(action) - - self._actions[_remove] = (job, variables) - self._update() - - return _remove - - async def async_run(self, hass: HomeAssistant, context: Context | None = None): - """Run all turn on triggers.""" - for job, variables in self._actions.values(): - hass.async_run_hass_job(job, variables, context) - - class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator to update data.""" @@ -125,8 +87,6 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): self.options = options self._notify_future: asyncio.Task | None = None - self.turn_on = PluggableAction(self.async_update_listeners) - super().__init__( hass, LOGGER, diff --git a/homeassistant/components/philips_js/const.py b/homeassistant/components/philips_js/const.py index 5d1141a8fb9..7788634ebc0 100644 --- a/homeassistant/components/philips_js/const.py +++ b/homeassistant/components/philips_js/const.py @@ -6,3 +6,5 @@ CONF_ALLOW_NOTIFY = "allow_notify" CONST_APP_ID = "homeassistant.io" CONST_APP_NAME = "Home Assistant" + +TRIGGER_TYPE_TURN_ON = "turn_on" diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index d7ce9807d64..bdf47674bc8 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -4,17 +4,18 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.trigger import ( + PluggableAction, + TriggerActionType, + TriggerInfo, +) from homeassistant.helpers.typing import ConfigType -from . import PhilipsTVDataUpdateCoordinator -from .const import DOMAIN - -TRIGGER_TYPE_TURN_ON = "turn_on" +from .const import DOMAIN, TRIGGER_TYPE_TURN_ON +from .helpers import async_get_turn_on_trigger TRIGGER_TYPES = {TRIGGER_TYPE_TURN_ON} TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -29,14 +30,7 @@ async def async_get_triggers( ) -> list[dict[str, str]]: """List device triggers for device.""" triggers = [] - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: TRIGGER_TYPE_TURN_ON, - } - ) + triggers.append(async_get_turn_on_trigger(device_id)) return triggers @@ -49,7 +43,6 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = trigger_info["trigger_data"] - registry: dr.DeviceRegistry = dr.async_get(hass) if (trigger_type := config[CONF_TYPE]) == TRIGGER_TYPE_TURN_ON: variables = { "trigger": { @@ -61,16 +54,9 @@ async def async_attach_trigger( } } - device = registry.async_get(config[CONF_DEVICE_ID]) - if device is None: - raise HomeAssistantError( - f"Device id {config[CONF_DEVICE_ID]} not found in registry" - ) - for config_entry_id in device.config_entries: - coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN].get( - config_entry_id - ) - if coordinator: - return coordinator.turn_on.async_attach(action, variables) + turn_on_trigger = async_get_turn_on_trigger(config[CONF_DEVICE_ID]) + return PluggableAction.async_attach_trigger( + hass, turn_on_trigger, action, variables + ) raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") diff --git a/homeassistant/components/philips_js/helpers.py b/homeassistant/components/philips_js/helpers.py new file mode 100644 index 00000000000..010ca7b9a19 --- /dev/null +++ b/homeassistant/components/philips_js/helpers.py @@ -0,0 +1,16 @@ +"""Helpers for philips_js.""" + +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE + +from .const import DOMAIN, TRIGGER_TYPE_TURN_ON + + +def async_get_turn_on_trigger(device_id: str) -> dict[str, str]: + """Return trigger description for a turn on trigger.""" + + return { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: TRIGGER_TYPE_TURN_ON, + } diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 116833d8a97..04e63008e7b 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -19,10 +19,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.trigger import PluggableAction from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator from .const import DOMAIN +from .helpers import async_get_turn_on_trigger SUPPORT_PHILIPS_JS = ( MediaPlayerEntityFeature.TURN_OFF @@ -39,8 +41,6 @@ SUPPORT_PHILIPS_JS = ( | MediaPlayerEntityFeature.STOP ) -CONF_ON_ACTION = "turn_on_action" - def _inverted(data): return {v: k for k, v in data.items()} @@ -95,21 +95,29 @@ class PhilipsTVMediaPlayer( self._media_title: str | None = None self._media_channel: str | None = None + self._turn_on = PluggableAction(self.async_write_ha_state) super().__init__(coordinator) self._update_from_coordinator() + async def async_added_to_hass(self) -> None: + """Handle being added to hass.""" + if (entry := self.registry_entry) and entry.device_id: + self.async_on_remove( + self._turn_on.async_register( + self.hass, async_get_turn_on_trigger(entry.device_id) + ) + ) + async def _async_update_soon(self): """Reschedule update task.""" self.async_write_ha_state() await self.coordinator.async_request_refresh() @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" supports = self._supports - if self.coordinator.turn_on or ( - self._tv.on and self._tv.powerstate is not None - ): + if self._turn_on or (self._tv.on and self._tv.powerstate is not None): supports |= MediaPlayerEntityFeature.TURN_ON return supports @@ -152,7 +160,7 @@ class PhilipsTVMediaPlayer( await self._tv.setPowerState("On") self._state = MediaPlayerState.ON else: - await self.coordinator.turn_on.async_run(self.hass, self._context) + await self._turn_on.async_run(self.hass, self._context) await self._async_update_soon() async def async_turn_off(self) -> None: diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 02d5e512a33..3496ec5f576 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -13,10 +13,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.trigger import PluggableAction from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER, PhilipsTVDataUpdateCoordinator from .const import DOMAIN +from .helpers import async_get_turn_on_trigger async def async_setup_entry( @@ -52,6 +54,16 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE name=coordinator.system["name"], sw_version=coordinator.system.get("softwareversion"), ) + self._turn_on = PluggableAction(self.async_write_ha_state) + + async def async_added_to_hass(self) -> None: + """Handle being added to hass.""" + if (entry := self.registry_entry) and entry.device_id: + self.async_on_remove( + self._turn_on.async_register( + self.hass, async_get_turn_on_trigger(entry.device_id) + ) + ) @property def is_on(self): @@ -65,7 +77,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") else: - await self.coordinator.turn_on.async_run(self.hass, self._context) + await self._turn_on.async_run(self.hass, self._context) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/pi_hole/translations/sk.json b/homeassistant/components/pi_hole/translations/sk.json index 4d37397c800..ee03e7a4993 100644 --- a/homeassistant/components/pi_hole/translations/sk.json +++ b/homeassistant/components/pi_hole/translations/sk.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "api_key": { "data": { @@ -12,9 +15,13 @@ "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", + "host": "Hostite\u013e", "location": "Umiestnenie", "name": "N\u00e1zov", - "port": "Port" + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "statistics_only": "Iba \u0161tatistika", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } } diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index a7d26ceb5c6..ec7f6e15425 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -8,6 +8,7 @@ from homeassistant.core import HomeAssistant from .const import CONF_API, CONF_COORDINATOR, CONF_COUNTRY_CODE, DOMAIN from .coordinator import PicnicUpdateCoordinator +from .services import async_register_services PLATFORMS = [Platform.SENSOR] @@ -36,6 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Register the services + await async_register_services(hass) + return True diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index f33f58c0eb9..85a7acadaeb 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -17,6 +17,14 @@ CONF_API = "api" CONF_COORDINATOR = "coordinator" CONF_COUNTRY_CODE = "country_code" +SERVICE_ADD_PRODUCT_TO_CART = "add_product" + +ATTR_CONFIG_ENTRY_ID = "config_entry_id" +ATTR_PRODUCT_ID = "product_id" +ATTR_PRODUCT_NAME = "product_name" +ATTR_AMOUNT = "amount" +ATTR_PRODUCT_IDENTIFIERS = "product_identifiers" + COUNTRY_CODES = ["NL", "DE", "BE"] ATTRIBUTION = "Data provided by Picnic" ADDRESS = "address" diff --git a/homeassistant/components/picnic/services.py b/homeassistant/components/picnic/services.py new file mode 100644 index 00000000000..3af2a521f8a --- /dev/null +++ b/homeassistant/components/picnic/services.py @@ -0,0 +1,92 @@ +"""Services for the Picnic integration.""" +from __future__ import annotations + +from typing import cast + +from python_picnic_api import PicnicAPI +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_AMOUNT, + ATTR_CONFIG_ENTRY_ID, + ATTR_PRODUCT_ID, + ATTR_PRODUCT_IDENTIFIERS, + ATTR_PRODUCT_NAME, + CONF_API, + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, +) + + +class PicnicServiceException(Exception): + """Exception for Picnic services.""" + + +async def async_register_services(hass: HomeAssistant) -> None: + """Register services for the Picnic integration, if not registered yet.""" + + if hass.services.has_service(DOMAIN, SERVICE_ADD_PRODUCT_TO_CART): + return + + async def async_add_product_service(call: ServiceCall): + api_client = await get_api_client(hass, call.data[ATTR_CONFIG_ENTRY_ID]) + await handle_add_product(hass, api_client, call) + + hass.services.async_register( + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, + async_add_product_service, + schema=vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string, + vol.Exclusive( + ATTR_PRODUCT_ID, ATTR_PRODUCT_IDENTIFIERS + ): cv.positive_int, + vol.Exclusive(ATTR_PRODUCT_NAME, ATTR_PRODUCT_IDENTIFIERS): cv.string, + vol.Optional(ATTR_AMOUNT): vol.All(vol.Coerce(int), vol.Range(min=1)), + } + ), + ) + + +async def get_api_client(hass: HomeAssistant, config_entry_id: str) -> PicnicAPI: + """Get the right Picnic API client based on the device id, else get the default one.""" + if config_entry_id not in hass.data[DOMAIN]: + raise ValueError(f"Config entry with id {config_entry_id} not found!") + return hass.data[DOMAIN][config_entry_id][CONF_API] + + +async def handle_add_product( + hass: HomeAssistant, api_client: PicnicAPI, call: ServiceCall +) -> None: + """Handle the call for the add_product service.""" + product_id = call.data.get("product_id") + if not product_id: + product_id = await hass.async_add_executor_job( + _product_search, api_client, cast(str, call.data["product_name"]) + ) + + if not product_id: + raise PicnicServiceException("No product found or no product ID given!") + + await hass.async_add_executor_job( + api_client.add_product, str(product_id), call.data.get("amount", 1) + ) + + +def _product_search(api_client: PicnicAPI, product_name: str) -> None | str: + """Query the api client for the product name.""" + search_result = api_client.search(product_name) + + if not search_result or "items" not in search_result[0]: + return None + + # Return the first valid result + for item in search_result[0]["items"]: + if "name" in item: + return str(item["id"]) + + return None diff --git a/homeassistant/components/picnic/services.yaml b/homeassistant/components/picnic/services.yaml new file mode 100644 index 00000000000..9af2cb48291 --- /dev/null +++ b/homeassistant/components/picnic/services.yaml @@ -0,0 +1,37 @@ +add_product: + name: Add a product to the cart + description: >- + Adds a product to the cart based on a search string or product ID. + The search string and product ID are exclusive. + + fields: + config_entry_id: + name: Picnic service + description: The product will be added to the selected service. + required: true + selector: + config_entry: + integration: picnic + product_id: + name: Product ID + description: The product ID of a Picnic product. + required: false + example: "10510201" + selector: + text: + product_name: + name: Product name + description: Search for a product and add the first result + required: false + example: "Yoghurt" + selector: + text: + amount: + name: Amount + description: Amount to add of the selected product + required: false + default: 1 + selector: + number: + min: 1 + max: 50 diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json index 24fa035f619..016286c9d2c 100644 --- a/homeassistant/components/picnic/translations/bg.json +++ b/homeassistant/components/picnic/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/picnic/translations/sk.json b/homeassistant/components/picnic/translations/sk.json index 1c63a1923bd..85830a679d5 100644 --- a/homeassistant/components/picnic/translations/sk.json +++ b/homeassistant/components/picnic/translations/sk.json @@ -1,15 +1,21 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "different_account": "\u00da\u010det by mal by\u0165 rovnak\u00fd ako pri nastavovan\u00ed integr\u00e1cie", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "country_code": "K\u00f3d krajiny" + "country_code": "K\u00f3d krajiny", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 0e1151e3dc4..e7c4eeea1ee 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -79,18 +79,17 @@ class PjLinkDevice(MediaPlayerEntity): """Iinitialize the PJLink device.""" self._host = host self._port = port - self._name = name + self._attr_name = name self._password = password self._encoding = encoding - self._muted = False - self._pwstate = MediaPlayerState.OFF - self._current_source = None + self._attr_is_volume_muted = False + self._attr_state = MediaPlayerState.OFF with self.projector() as projector: - if not self._name: - self._name = projector.get_name() + if not self._attr_name: + self._attr_name = projector.get_name() inputs = projector.get_inputs() self._source_name_mapping = {format_input_source(*x): x for x in inputs} - self._source_list = sorted(self._source_name_mapping.keys()) + self._attr_source_list = sorted(self._source_name_mapping.keys()) def projector(self): """Create PJLink Projector instance.""" @@ -108,53 +107,28 @@ class PjLinkDevice(MediaPlayerEntity): try: pwstate = projector.get_power() if pwstate in ("on", "warm-up"): - self._pwstate = MediaPlayerState.ON - self._muted = projector.get_mute()[1] - self._current_source = format_input_source(*projector.get_input()) + self._attr_state = MediaPlayerState.ON + self._attr_is_volume_muted = projector.get_mute()[1] + self._attr_source = format_input_source(*projector.get_input()) else: - self._pwstate = MediaPlayerState.OFF - self._muted = False - self._current_source = None + self._attr_state = MediaPlayerState.OFF + self._attr_is_volume_muted = False + self._attr_source = None except KeyError as err: if str(err) == "'OK'": - self._pwstate = MediaPlayerState.OFF - self._muted = False - self._current_source = None + self._attr_state = MediaPlayerState.OFF + self._attr_is_volume_muted = False + self._attr_source = None else: raise except ProjectorError as err: if str(err) == "unavailable time": - self._pwstate = MediaPlayerState.OFF - self._muted = False - self._current_source = None + self._attr_state = MediaPlayerState.OFF + self._attr_is_volume_muted = False + self._attr_source = None else: raise - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._pwstate - - @property - def is_volume_muted(self): - """Return boolean indicating mute status.""" - return self._muted - - @property - def source(self): - """Return current input source.""" - return self._current_source - - @property - def source_list(self): - """Return all available input sources.""" - return self._source_list - def turn_off(self) -> None: """Turn projector off.""" with self.projector() as projector: diff --git a/homeassistant/components/plaato/translations/sk.json b/homeassistant/components/plaato/translations/sk.json new file mode 100644 index 00000000000..c8633ec2895 --- /dev/null +++ b/homeassistant/components/plaato/translations/sk.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + }, + "create_entry": { + "default": "Va\u0161e zariadenie Plaato {device_type} s n\u00e1zvom **{device_name}** bolo \u00faspe\u0161ne nastaven\u00e9!" + }, + "error": { + "no_auth_token": "Mus\u00edte prida\u0165 autoriza\u010dn\u00fd token" + }, + "step": { + "api_method": { + "data": { + "token": "Sem vlo\u017ete autoriza\u010dn\u00fd token" + }, + "title": "Vyberte met\u00f3du API" + }, + "user": { + "data": { + "device_name": "Pomenujte svoje zariadenie", + "device_type": "Typ zariadenia Plaato" + }, + "description": "Chcete za\u010da\u0165 nastavova\u0165?", + "title": "Nastavte zariadenia Plaato" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Interval aktualiz\u00e1cie (v min\u00fatach)" + }, + "description": "Nastavte interval aktualiz\u00e1cie (v min\u00fatach)", + "title": "Mo\u017enosti pre Plaato" + }, + "webhook": { + "description": "Inform\u00e1cie o webhooku: \n\n - URL: `{webhook_url}`\n - Met\u00f3da: POST \n\n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plant/translations/hu.json b/homeassistant/components/plant/translations/hu.json index ad2061411f5..dbfa2b73fe1 100644 --- a/homeassistant/components/plant/translations/hu.json +++ b/homeassistant/components/plant/translations/hu.json @@ -5,5 +5,5 @@ "problem": "Probl\u00e9ma" } }, - "title": "N\u00f6v\u00e9nyfigyel\u0151" + "title": "Er\u0151m\u0171 monitoroz\u00e1s" } \ No newline at end of file diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 171648042c1..8eb1e3d6903 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.13.0", + "plexapi==4.13.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 84e0f084210..b43c4dc0e21 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -389,7 +389,7 @@ class PlexMediaPlayer(MediaPlayerEntity): return self.session.media_episode @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self.device and "playback" in self._device_protocol_capabilities: return ( diff --git a/homeassistant/components/plex/translations/bg.json b/homeassistant/components/plex/translations/bg.json index 0e39e4b8b04..b0927813fa6 100644 --- a/homeassistant/components/plex/translations/bg.json +++ b/homeassistant/components/plex/translations/bg.json @@ -4,7 +4,7 @@ "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/plex/translations/sk.json b/homeassistant/components/plex/translations/sk.json index 68438cbdfb0..db7c9aeb581 100644 --- a/homeassistant/components/plex/translations/sk.json +++ b/homeassistant/components/plex/translations/sk.json @@ -1,17 +1,44 @@ { "config": { "abort": { + "all_configured": "V\u0161etky prepojen\u00e9 servery s\u00fa u\u017e nakonfigurovan\u00e9", "already_configured": "Tento Plex server u\u017e je nakonfigurovan\u00fd", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "token_request_timeout": "\u010casov\u00fd limit pri z\u00edskavan\u00ed tokenu vypr\u0161al", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "error": { + "faulty_credentials": "Autoriz\u00e1cia zlyhala, overte token", + "host_or_token": "Mus\u00ed poskytn\u00fa\u0165 aspo\u0148 jeden z hostite\u013eov alebo tokenov", + "no_servers": "\u017diadne servery prepojen\u00e9 s \u00fa\u010dtom Plex", + "not_found": "Plex server sa nena\u0161iel", + "ssl_error": "Probl\u00e9m s certifik\u00e1tom SSL" + }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { + "host": "Hostite\u013e", "port": "Port", "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "token": "Token (volite\u013en\u00fd)", "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + }, + "title": "Manu\u00e1lna konfigur\u00e1cia Plex" + }, + "select_server": { + "data": { + "server": "Server" + }, + "title": "Vyberte server Plex" + }, + "user": { + "description": "Pokra\u010dujte na [plex.tv](https://plex.tv) a prepojte server Plex." + }, + "user_advanced": { + "data": { + "setup_method": "Sp\u00f4sob nastavenia" } } } diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 164135a607b..10e4836f73a 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -36,6 +36,12 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( icon_off="mdi:hvac-off", entity_category=EntityCategory.DIAGNOSTIC, ), + PlugwiseBinarySensorEntityDescription( + key="cooling_enabled", + name="Cooling enabled", + icon="mdi:snowflake-thermometer", + entity_category=EntityCategory.DIAGNOSTIC, + ), PlugwiseBinarySensorEntityDescription( key="dhw_state", name="DHW state", diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 09b919e9d1d..3bbd5725b29 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -4,9 +4,12 @@ from __future__ import annotations from typing import Any from plugwise.exceptions import ( + ConnectionFailedError, InvalidAuthentication, InvalidSetupError, - PlugwiseException, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, ) from plugwise.smile import Smile import voluptuous as vol @@ -32,7 +35,6 @@ from .const import ( DOMAIN, FLOW_SMILE, FLOW_STRETCH, - LOGGER, PW_TYPE, SMILE, STRETCH, @@ -175,14 +177,17 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): try: api = await validate_gw_input(self.hass, user_input) - except InvalidSetupError: - errors[CONF_BASE] = "invalid_setup" + except ConnectionFailedError: + errors[CONF_BASE] = "cannot_connect" except InvalidAuthentication: errors[CONF_BASE] = "invalid_auth" - except PlugwiseException: - errors[CONF_BASE] = "cannot_connect" + except InvalidSetupError: + errors[CONF_BASE] = "invalid_setup" + except (InvalidXMLError, ResponseError): + errors[CONF_BASE] = "response_error" + except UnsupportedDeviceError: + errors[CONF_BASE] = "unsupported" except Exception: # pylint: disable=broad-except - LOGGER.exception("Unexpected exception") errors[CONF_BASE] = "unknown" else: await self.async_set_unique_id( diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 6fd44efda84..30adca12819 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -4,13 +4,23 @@ from typing import NamedTuple, cast from plugwise import Smile from plugwise.constants import DeviceData, GatewayData -from plugwise.exceptions import PlugwiseException, XMLDataMissingError +from plugwise.exceptions import ( + ConnectionFailedError, + InvalidAuthentication, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER +from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, LOGGER class PlugwiseData(NamedTuple): @@ -23,15 +33,15 @@ class PlugwiseData(NamedTuple): class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): """Class to manage fetching Plugwise data from single endpoint.""" - def __init__(self, hass: HomeAssistant, api: Smile) -> None: + _connected: bool = False + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the coordinator.""" super().__init__( hass, LOGGER, - name=api.smile_name or DOMAIN, - update_interval=DEFAULT_SCAN_INTERVAL.get( - str(api.smile_type), timedelta(seconds=60) - ), + name=DOMAIN, + update_interval=timedelta(seconds=60), # Don't refresh immediately, give the device time to process # the change in state before we query it. request_refresh_debouncer=Debouncer( @@ -41,18 +51,41 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): immediate=False, ), ) - self.api = api + + self.api = Smile( + host=entry.data[CONF_HOST], + username=entry.data.get(CONF_USERNAME, DEFAULT_USERNAME), + password=entry.data[CONF_PASSWORD], + port=entry.data.get(CONF_PORT, DEFAULT_PORT), + timeout=30, + websession=async_get_clientsession(hass, verify_ssl=False), + ) + + async def _connect(self) -> None: + """Connect to the Plugwise Smile.""" + self._connected = await self.api.connect() + self.api.get_all_devices() + self.name = self.api.smile_name + self.update_interval = DEFAULT_SCAN_INTERVAL.get( + str(self.api.smile_type), timedelta(seconds=60) + ) async def _async_update_data(self) -> PlugwiseData: """Fetch data from Plugwise.""" try: + if not self._connected: + await self._connect() data = await self.api.async_update() - except XMLDataMissingError as err: + except InvalidAuthentication as err: + raise ConfigEntryError("Invalid username or Smile ID") from err + except (InvalidXMLError, ResponseError) as err: raise UpdateFailed( - f"No XML data received for: {self.api.smile_name}" + "Invalid XML data, or error indication received for the Plugwise Adam/Smile/Stretch" ) from err - except PlugwiseException as err: - raise UpdateFailed(f"Updated failed for: {self.api.smile_name}") from err + except UnsupportedDeviceError as err: + raise ConfigEntryError("Device with unsupported firmware") from err + except ConnectionFailedError as err: + raise UpdateFailed("Failed to connect to the Plugwise Smile") from err return PlugwiseData( gateway=cast(GatewayData, data[0]), devices=cast(dict[str, DeviceData], data[1]), diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 16b6a977569..57db57e0e8f 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -1,28 +1,13 @@ """Plugwise platform for Home Assistant Core.""" from __future__ import annotations -import asyncio from typing import Any -from aiohttp import ClientConnectionError -from plugwise.exceptions import InvalidAuthentication, PlugwiseException -from plugwise.smile import Smile - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ( - DEFAULT_PORT, - DEFAULT_USERNAME, - DOMAIN, - LOGGER, - PLATFORMS_GATEWAY, - Platform, -) +from .const import DOMAIN, LOGGER, PLATFORMS_GATEWAY, Platform from .coordinator import PlugwiseDataUpdateCoordinator @@ -30,38 +15,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise Smiles from a config entry.""" await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) - websession = async_get_clientsession(hass, verify_ssl=False) - api = Smile( - host=entry.data[CONF_HOST], - username=entry.data.get(CONF_USERNAME, DEFAULT_USERNAME), - password=entry.data[CONF_PASSWORD], - port=entry.data.get(CONF_PORT, DEFAULT_PORT), - timeout=30, - websession=websession, - ) - - try: - connected = await api.connect() - except InvalidAuthentication: - LOGGER.error("Invalid username or Smile ID") - return False - except (ClientConnectionError, PlugwiseException) as err: - raise ConfigEntryNotReady( - f"Error while communicating to device {api.smile_name}" - ) from err - except asyncio.TimeoutError as err: - raise ConfigEntryNotReady( - f"Timeout while connecting to Smile {api.smile_name}" - ) from err - - if not connected: - raise ConfigEntryNotReady("Unable to connect to Smile") - api.get_all_devices() - - if entry.unique_id is None and api.smile_version[0] != "1.8.0": - hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname) - - coordinator = PlugwiseDataUpdateCoordinator(hass, api) + coordinator = PlugwiseDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() migrate_sensor_entities(hass, coordinator) @@ -70,11 +24,11 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - identifiers={(DOMAIN, str(api.gateway_id))}, + identifiers={(DOMAIN, str(coordinator.api.gateway_id))}, manufacturer="Plugwise", - model=api.smile_model, - name=api.smile_name, - sw_version=api.smile_version[0], + model=coordinator.api.smile_model, + name=coordinator.api.smile_name, + sw_version=coordinator.api.smile_version[0], ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 6bb1c941bf3..e10d86f2779 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.25.7"], + "requirements": ["plugwise==0.25.14"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 9100e006968..0b8c7f820b4 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -71,7 +71,7 @@ async def async_setup_entry( entities: list[PlugwiseNumberEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in NUMBER_TYPES: - if description.key in device: + if description.key in device and "setpoint" in device[description.key]: entities.append( PlugwiseNumberEntity(coordinator, device_id, description) ) diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index b6e2a3aa413..a59eb0cfc39 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -301,6 +301,22 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="domestic_hot_water_setpoint", + name="DHW setpoint", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="maximum_boiler_temperature", + name="Maximum boiler temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + ), ) diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 7278f6c4414..781a17a1d10 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -15,8 +15,10 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_setup": "Add your Adam instead of your Anna, see the documentation", + "response_error": "Invalid XML data, or error indication received", + "unknown": "[%key:common::config_flow::error::unknown%]", + "unsupported": "Device with unsupported firmware" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", diff --git a/homeassistant/components/plugwise/translations/bg.json b/homeassistant/components/plugwise/translations/bg.json index 18450edfce7..c74eb0a71ac 100644 --- a/homeassistant/components/plugwise/translations/bg.json +++ b/homeassistant/components/plugwise/translations/bg.json @@ -6,9 +6,9 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d \u0444\u044a\u0440\u043c\u0443\u0435\u0440" }, - "flow_title": "{name}", "step": { "user": { "data": { @@ -16,12 +16,6 @@ "port": "\u041f\u043e\u0440\u0442" }, "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:" - }, - "user_gateway": { - "data": { - "host": "IP \u0430\u0434\u0440\u0435\u0441", - "port": "\u041f\u043e\u0440\u0442" - } } } } diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index ccd3d344f39..b018a764124 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -8,13 +8,12 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_setup": "Afegeix l'Adam en lloc de l'Anna; consulta la documentaci\u00f3 de la integraci\u00f3 Plugwise de Home Assistant per a m\u00e9s informaci\u00f3.", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unsupported": "Dispositiu amb programari no compatible" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Tipus de connexi\u00f3", "host": "Adre\u00e7a IP", "password": "ID de Smile", "port": "Port", @@ -22,26 +21,6 @@ }, "description": "Introdueix", "title": "Connexi\u00f3 amb Smile" - }, - "user_gateway": { - "data": { - "host": "Adre\u00e7a IP", - "password": "ID de Smile", - "port": "Port", - "username": "Nom d'usuari de Smile" - }, - "description": "Introdueix", - "title": "Connexi\u00f3 amb Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Interval d'escaneig (segons)" - }, - "description": "Ajusta les opcions de Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json index a7f5dae7c97..02a806f4548 100644 --- a/homeassistant/components/plugwise/translations/cs.json +++ b/homeassistant/components/plugwise/translations/cs.json @@ -6,37 +6,18 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "invalid_setup": "P\u0159idejte sv\u00e9ho Adama m\u00edsto sv\u00e9 Anny, viz dokumentace", + "response_error": "Byla p\u0159ijata neplatn\u00e1 data XML nebo indikace chyby", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "unsupported": "Za\u0159\u00edzen\u00ed s nepodporovan\u00fdm firmwarem" }, - "flow_title": "Smile: {name}", "step": { "user": { "data": { - "flow_type": "Typ p\u0159ipojen\u00ed", "host": "IP adresa" }, "description": "Produkt:", "title": "Typ Plugwise" - }, - "user_gateway": { - "data": { - "host": "IP adresa", - "password": "Smile ID", - "port": "Port", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no Smile" - }, - "description": "Pros\u00edm zadejte", - "title": "P\u0159ipojen\u00ed k Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Interval sledov\u00e1n\u00ed (v sekund\u00e1ch)" - }, - "description": "Upravte mo\u017enosti Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index fb80fecef25..6aeba3e5605 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "invalid_setup": "F\u00fcge deinen Adam anstelle deiner Anna hinzu. Weitere Informationen findest du in der Dokumentation zur Integration von Home Assistant Plugwise.", - "unknown": "Unerwarteter Fehler" + "invalid_setup": "F\u00fcge deinen Adam anstelle deiner Anna hinzu, siehe Dokumentation.", + "response_error": "Ung\u00fcltige XML-Daten oder Fehleranzeige empfangen", + "unknown": "Unerwarteter Fehler", + "unsupported": "Ger\u00e4t mit nicht unterst\u00fctzter Firmware" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Verbindungstyp", "host": "IP-Adresse", "password": "Smile ID", "port": "Port", @@ -22,26 +22,6 @@ }, "description": "Bitte eingeben", "title": "Stelle eine Verbindung zu Smile her" - }, - "user_gateway": { - "data": { - "host": "IP-Adresse", - "password": "Smile ID", - "port": "Port", - "username": "Smile-Benutzername" - }, - "description": "Bitte eingeben", - "title": "Stelle eine Verbindung zu Smile her" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Scanintervall (Sekunden)" - }, - "description": "Plugwise-Optionen einstellen" } } } diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index 18a50e86b66..11907c3bd03 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -8,13 +8,13 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_setup": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Adam \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd Anna \u03c3\u03b1\u03c2, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant Plugwise \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "response_error": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 XML \u03ae \u03bb\u03b7\u03c6\u03b8\u03b5\u03af\u03c3\u03b1 \u03ad\u03bd\u03b4\u03b5\u03b9\u03be\u03b7 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03bf\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unsupported": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "password": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc Smile", "port": "\u0398\u03cd\u03c1\u03b1", @@ -22,26 +22,6 @@ }, "description": "\u03a0\u03c1\u03bf\u03ca\u03cc\u03bd:", "title": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b2\u03cd\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2" - }, - "user_gateway": { - "data": { - "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", - "password": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc Smile", - "port": "\u0398\u03cd\u03c1\u03b1", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Smile" - }, - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5", - "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" - }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03ad\u03c4\u03b7\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index cd10502d0c3..ecc5af9218d 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", - "unknown": "Unexpected error" + "invalid_setup": "Add your Adam instead of your Anna, see the documentation", + "response_error": "Invalid XML data, or error indication received", + "unknown": "Unexpected error", + "unsupported": "Device with unsupported firmware" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Connection type", "host": "IP Address", "password": "Smile ID", "port": "Port", @@ -22,26 +22,6 @@ }, "description": "Please enter", "title": "Connect to the Smile" - }, - "user_gateway": { - "data": { - "host": "IP Address", - "password": "Smile ID", - "port": "Port", - "username": "Smile Username" - }, - "description": "Please enter", - "title": "Connect to the Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Scan Interval (seconds)" - }, - "description": "Adjust Plugwise Options" } } } diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index fed26040384..9797cffa43c 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_setup": "A\u00f1ade tu Adam en lugar de tu Anna, consulta la documentaci\u00f3n de la integraci\u00f3n Home Assistant Plugwise para m\u00e1s informaci\u00f3n", - "unknown": "Error inesperado" + "invalid_setup": "A\u00f1ade tu Adam en lugar de tu Anna, consulta la documentaci\u00f3n", + "response_error": "Datos XML no v\u00e1lidos o indicaci\u00f3n de error recibida", + "unknown": "Error inesperado", + "unsupported": "Dispositivo con firmware no compatible" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Tipo de conexi\u00f3n", "host": "Direcci\u00f3n IP", "password": "ID de Smile", "port": "Puerto", @@ -22,26 +22,6 @@ }, "description": "Por favor, introduce", "title": "Conectar a Smile" - }, - "user_gateway": { - "data": { - "host": "Direcci\u00f3n IP", - "password": "ID de Smile", - "port": "Puerto", - "username": "Nombre de usuario Smile" - }, - "description": "Por favor, introduce", - "title": "Conectar a Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Intervalo de escaneo (segundos)" - }, - "description": "Ajustar las opciones de Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 9f2f2e0b1b6..3ed7329fbb1 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", - "invalid_setup": "Lisa oma Anna asemel oma Adam, lisateabe saamiseks vaata Home Assistant Plugwise'i sidumise dokumentatsiooni", - "unknown": "Tundmatu viga" + "invalid_setup": "Lisa oma Anna asemel oma Adam, lisateabe saamiseks vaata dokumentatsiooni", + "response_error": "Vigased XML-andmed v\u00f5i saadud veam\u00e4rguanne", + "unknown": "Tundmatu viga", + "unsupported": "Toetamata p\u00fcsivaraga seade" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "\u00dchenduse t\u00fc\u00fcp", "host": "IP aadress", "password": "Smile ID", "port": "Port", @@ -22,26 +22,6 @@ }, "description": "Sisesta andmed", "title": "Loo \u00fchendus Smile-ga" - }, - "user_gateway": { - "data": { - "host": "IP aadress", - "password": "Smile ID", - "port": "Port", - "username": "Smile kasutajanimi" - }, - "description": "Palun sisesta:", - "title": "Loo \u00fchendus Smile-ga" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "P\u00e4ringute intervall (sekundites)" - }, - "description": "Kohanda Plugwise s\u00e4tteid" } } } diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 85f4f652c18..ee2b91b189e 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -10,11 +10,9 @@ "invalid_setup": "Ajoutez votre Adam au lieu de votre Anna\u00a0; consultez la documentation de l'int\u00e9gration Plugwise de Home Assistant pour plus d'informations", "unknown": "Erreur inattendue" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Type de connexion", "host": "Adresse IP", "password": "ID Smile", "port": "Port", @@ -22,26 +20,6 @@ }, "description": "Veuillez saisir", "title": "Se connecter \u00e0 Smile" - }, - "user_gateway": { - "data": { - "host": "Adresse IP", - "password": "ID Smile", - "port": "Port", - "username": "Nom d'utilisateur Smile" - }, - "description": "Veuillez saisir :", - "title": "Se connecter \u00e0 Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Intervalle de mise \u00e0 jour (secondes)" - }, - "description": "Ajuster les options Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json index f8a4c722a8f..ed7adcac577 100644 --- a/homeassistant/components/plugwise/translations/he.json +++ b/homeassistant/components/plugwise/translations/he.json @@ -8,7 +8,6 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{name}", "step": { "user": { "data": { @@ -16,14 +15,6 @@ "port": "\u05e4\u05ea\u05d7\u05d4" }, "description": "\u05de\u05d5\u05e6\u05e8:" - }, - "user_gateway": { - "data": { - "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", - "password": "\u05de\u05d6\u05d4\u05d4 Smile", - "port": "\u05e4\u05ea\u05d7\u05d4", - "username": "\u05d7\u05d9\u05d9\u05da \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } } } } diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index c97911bad09..cdfb76fcc5c 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -10,11 +10,9 @@ "invalid_setup": "Adja hozz\u00e1 Adamot Anna helyett. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse meg a Home Assistant Plugwise integr\u00e1ci\u00f3s dokument\u00e1ci\u00f3j\u00e1t", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Kapcsolat t\u00edpusa", "host": "IP c\u00edm", "password": "Smile azonos\u00edt\u00f3", "port": "Port", @@ -22,26 +20,6 @@ }, "description": "K\u00e9rem, adja meg", "title": "Csatlakoz\u00e1s a Smile-hoz" - }, - "user_gateway": { - "data": { - "host": "IP c\u00edm", - "password": "Smile azonos\u00edt\u00f3", - "port": "Port", - "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "description": "K\u00e9rem, adja meg", - "title": "Csatlakoz\u00e1s a Smile-hoz" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" - }, - "description": "\u00c1ll\u00edtsa be a Plugwise lehet\u0151s\u00e9get" } } } diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index daa2824df27..71578c7462c 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -8,13 +8,13 @@ "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "invalid_setup": "Tambahkan Adam Anda, alih-alih Anna. Baca dokumentasi integrasi Plugwise Home Assistant untuk informasi lebih lanjut", - "unknown": "Kesalahan yang tidak diharapkan" + "response_error": "Data XML tidak valid, atau indikasi kesalahan diterima", + "unknown": "Kesalahan yang tidak diharapkan", + "unsupported": "Perangkat dengan firmware yang tidak didukung" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Jenis koneksi", "host": "Alamat IP", "password": "ID Smile", "port": "Port", @@ -22,26 +22,6 @@ }, "description": "Masukkan", "title": "Hubungkan ke Smile" - }, - "user_gateway": { - "data": { - "host": "Alamat IP", - "password": "ID Smile", - "port": "Port", - "username": "Nama Pengguna Smile" - }, - "description": "Masukkan", - "title": "Hubungkan ke Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Interval Pindai (detik)" - }, - "description": "Sesuaikan Opsi Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index e4bf239b7bc..8d0555a12a7 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -10,11 +10,9 @@ "invalid_setup": "Aggiungi il tuo Adam invece di Anna, consulta la documentazione sull'integrazione di Home Assistant Plugwise per ulteriori informazioni", "unknown": "Errore imprevisto" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Tipo di connessione", "host": "Indirizzo IP", "password": "ID Smile", "port": "Porta", @@ -22,26 +20,6 @@ }, "description": "Inserisci", "title": "Connettiti allo Smile" - }, - "user_gateway": { - "data": { - "host": "Indirizzo IP", - "password": "ID Smile", - "port": "Porta", - "username": "Nome utente Smile" - }, - "description": "Inserisci", - "title": "Connettiti allo Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Intervallo di scansione (secondi)" - }, - "description": "Regolare le opzioni Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 15eb388d2c8..bc7ea107a5e 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -10,11 +10,9 @@ "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u7d71\u5408\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7", "host": "IP\u30a2\u30c9\u30ec\u30b9", "password": "Smile\u306eID", "port": "\u30dd\u30fc\u30c8", @@ -22,26 +20,6 @@ }, "description": "\u30d7\u30ed\u30c0\u30af\u30c8:", "title": "Plugwise type" - }, - "user_gateway": { - "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9", - "password": "Smile ID", - "port": "\u30dd\u30fc\u30c8", - "username": "Smile \u30e6\u30fc\u30b6\u30fc\u540d" - }, - "description": "\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Smile\u306b\u63a5\u7d9a" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" - }, - "description": "Plugwise\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8abf\u6574" } } } diff --git a/homeassistant/components/plugwise/translations/ka.json b/homeassistant/components/plugwise/translations/ka.json deleted file mode 100644 index d4b446b309c..00000000000 --- a/homeassistant/components/plugwise/translations/ka.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "flow_type": "\u1c99\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8" - } - }, - "user_gateway": { - "data": { - "host": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8", - "password": "Smile ID", - "port": "\u10de\u10dd\u10e0\u10e2\u10d8", - "username": "\u10e6\u10d8\u10db\u10d8\u10da\u10d8\u10d0\u10dc\u10d8 \u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10da\u10d8\u10e1 \u10e1\u10d0\u10ee\u10d4\u10da\u10d8" - }, - "description": "\u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10dd\u10d7", - "title": "\u10d3\u10d0\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d8\u10d7 Smile-\u10e1" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json index 7d856d1fe28..5873415b066 100644 --- a/homeassistant/components/plugwise/translations/ko.json +++ b/homeassistant/components/plugwise/translations/ko.json @@ -8,34 +8,10 @@ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "Smile: {name}", "step": { "user": { - "data": { - "flow_type": "\uc5f0\uacb0 \uc720\ud615" - }, "description": "\uc81c\ud488:", "title": "Plugwise \uc720\ud615" - }, - "user_gateway": { - "data": { - "host": "IP \uc8fc\uc18c", - "password": "Smile ID", - "port": "\ud3ec\ud2b8", - "username": "Smile \uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "description": "\uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Smile\uc5d0 \uc5f0\uacb0\ud558\uae30" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" - }, - "description": "Plugwise \uc635\uc158 \uc870\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/plugwise/translations/lb.json b/homeassistant/components/plugwise/translations/lb.json index a3618bc911e..8c160412faa 100644 --- a/homeassistant/components/plugwise/translations/lb.json +++ b/homeassistant/components/plugwise/translations/lb.json @@ -8,33 +8,10 @@ "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, - "flow_title": "Smile: {name}", "step": { "user": { - "data": { - "flow_type": "Typ vun der Verbindung" - }, "description": "Produkt:", "title": "Typ vu Plugwise" - }, - "user_gateway": { - "data": { - "host": "IP Adress", - "password": "Smile ID", - "port": "Port", - "username": "Smile Benotzernumm" - }, - "title": "Mam Smile verbannen" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Scan Intervall (sekonnen)" - }, - "description": "Plugwise Optioune \u00e4nneren" } } } diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index 14d25d6716e..5a7e0dc687f 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -7,13 +7,13 @@ "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "invalid_setup": "Voeg je Adam toe in plaats van je Anna, zie de Home Assistant Plugwise integratiedocumentatie voor meer informatie", - "unknown": "Onverwachte fout" + "response_error": "Ongeldige XML-gegevens, of foutindicatie ontvangen", + "unknown": "Onverwachte fout", + "unsupported": "Apparaat met niet ondersteunde firmware" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Verbindingstype", "host": "IP-adres", "password": "Smile-ID", "port": "Poort", @@ -21,26 +21,6 @@ }, "description": "Product:", "title": "Plugwise type" - }, - "user_gateway": { - "data": { - "host": "IP-adres", - "password": "Smile-ID", - "port": "Poort", - "username": "Smile gebruikersnaam" - }, - "description": "Voer in", - "title": "Maak verbinding met de Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Scaninterval (seconden)" - }, - "description": "Plugwise opties aanpassen" } } } diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index ad95ab8e4ee..ea676591361 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", - "invalid_setup": "Legg til din Adam i stedet for din Anna, se integrasjonsdokumentasjonen for Home Assistant Plugwise for mer informasjon", - "unknown": "Uventet feil" + "invalid_setup": "Legg til din Adam i stedet for din Anna, se dokumentasjonen", + "response_error": "Ugyldige XML-data, eller feilindikasjon mottatt", + "unknown": "Uventet feil", + "unsupported": "Enhet med fastvare som ikke st\u00f8ttes" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Tilkoblingstype", "host": "IP adresse", "password": "Smile ID", "port": "Port", @@ -22,26 +22,6 @@ }, "description": "Vennligst skriv inn", "title": "Koble til Smile" - }, - "user_gateway": { - "data": { - "host": "IP adresse", - "password": "Smile ID", - "port": "Port", - "username": "Smile brukernavn" - }, - "description": "Vennligst skriv inn", - "title": "Koble til Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Skanneintervall (sekunder)" - }, - "description": "Juster Plugwise-alternativer" } } } diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 8de8da8e4e9..cfbd6009a37 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -10,11 +10,9 @@ "invalid_setup": "Dodaj urz\u0105dzenie Adam zamiast Anna. Zobacz dokumentacj\u0119 integracji Plugwise dla Home Assistant, aby uzyska\u0107 wi\u0119cej informacji.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Typ po\u0142\u0105czenia", "host": "Adres IP", "password": "Identyfikator Smile", "port": "Port", @@ -22,26 +20,6 @@ }, "description": "Wprowad\u017a:", "title": "Po\u0142\u0105czenie ze Smile" - }, - "user_gateway": { - "data": { - "host": "Adres IP", - "password": "Identyfikator Smile", - "port": "Port", - "username": "Nazwa u\u017cytkownika" - }, - "description": "Wprowad\u017a:", - "title": "Po\u0142\u0105czenie ze Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)" - }, - "description": "Dostosowywanie opcji Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index 5f667f8d6ff..edb638bae7a 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "invalid_setup": "Adicione seu Adam em vez de sua Anna, consulte a documenta\u00e7\u00e3o de integra\u00e7\u00e3o do Home Assistant Plugwise para obter mais informa\u00e7\u00f5es", - "unknown": "Erro inesperado" + "invalid_setup": "Adicione seu Adam em vez de sua Anna, veja a documenta\u00e7\u00e3o", + "response_error": "Dados XML inv\u00e1lidos ou indica\u00e7\u00e3o de erro recebido", + "unknown": "Erro inesperado", + "unsupported": "Dispositivo com firmware n\u00e3o suportado" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Tipo de conex\u00e3o", "host": "Endere\u00e7o IP", "password": "ID do Smile", "port": "Porta", @@ -22,26 +22,6 @@ }, "description": "Por favor, insira", "title": "Conecte-se ao Smile" - }, - "user_gateway": { - "data": { - "host": "Endere\u00e7o IP", - "password": "ID do Smile", - "port": "Porta", - "username": "Nome de usu\u00e1rio Smile" - }, - "description": "Por favor, insira", - "title": "Conecte-se ao Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Intervalo de escaneamento (segundos)" - }, - "description": "Ajustar as op\u00e7\u00f5es Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/pt.json b/homeassistant/components/plugwise/translations/pt.json index dd40927a7c7..3481b1de025 100644 --- a/homeassistant/components/plugwise/translations/pt.json +++ b/homeassistant/components/plugwise/translations/pt.json @@ -7,23 +7,6 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" - }, - "step": { - "user_gateway": { - "data": { - "host": "Endere\u00e7o IP", - "port": "Porta" - } - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o (segundos)" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index a6a31b7b63a..21b3c00a99a 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -7,14 +7,14 @@ "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_setup": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 Adam \u0432\u043c\u0435\u0441\u0442\u043e Anna. \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 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Plugwise \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "invalid_setup": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 Adam \u0432\u043c\u0435\u0441\u0442\u043e Anna. \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 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", + "response_error": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 XML, \u0438\u043b\u0438 \u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043e\u0448\u0438\u0431\u043a\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u043e\u0439." }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "host": "IP-\u0430\u0434\u0440\u0435\u0441", "password": "Smile ID", "port": "\u041f\u043e\u0440\u0442", @@ -22,26 +22,6 @@ }, "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Smile" - }, - "user_gateway": { - "data": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441", - "password": "Smile ID", - "port": "\u041f\u043e\u0440\u0442", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile" - }, - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435:", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Plugwise" } } } diff --git a/homeassistant/components/plugwise/translations/select.cs.json b/homeassistant/components/plugwise/translations/select.cs.json new file mode 100644 index 00000000000..2eef80268c6 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.cs.json @@ -0,0 +1,7 @@ +{ + "state": { + "plugwise__dhw_mode": { + "off": "Vypnuto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.el.json b/homeassistant/components/plugwise/translations/select.el.json index 88f8e117b48..10dc2853ef4 100644 --- a/homeassistant/components/plugwise/translations/select.el.json +++ b/homeassistant/components/plugwise/translations/select.el.json @@ -1,5 +1,11 @@ { "state": { + "plugwise__dhw_mode": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "boost": "\u0395\u03bd\u03af\u03c3\u03c7\u03c5\u03c3\u03b7", + "comfort": "\u0386\u03bd\u03b5\u03c3\u03b7", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + }, "plugwise__regulation_mode": { "bleeding_cold": "\u0391\u03b9\u03bc\u03bf\u03c1\u03c1\u03b1\u03b3\u03af\u03b1 \u03ba\u03c1\u03cd\u03b1", "bleeding_hot": "\u0391\u03b9\u03bc\u03bf\u03c1\u03c1\u03b1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c5\u03c4\u03ae", diff --git a/homeassistant/components/plugwise/translations/select.hr.json b/homeassistant/components/plugwise/translations/select.hr.json new file mode 100644 index 00000000000..65d546a205d --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.hr.json @@ -0,0 +1,17 @@ +{ + "state": { + "plugwise__dhw_mode": { + "auto": "Auto", + "boost": "Poja\u010dano", + "comfort": "Udobnost", + "off": "Isklju\u010deno" + }, + "plugwise__regulation_mode": { + "bleeding_cold": "Jako hladno", + "bleeding_hot": "Jako vru\u0107e", + "cooling": "Hla\u0111enje", + "heating": "Grijanje", + "off": "Isklju\u010deno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.id.json b/homeassistant/components/plugwise/translations/select.id.json index 0be50360a0f..3eef4c30aa5 100644 --- a/homeassistant/components/plugwise/translations/select.id.json +++ b/homeassistant/components/plugwise/translations/select.id.json @@ -1,5 +1,11 @@ { "state": { + "plugwise__dhw_mode": { + "auto": "Otomatis", + "boost": "Kencang", + "comfort": "Nyaman", + "off": "Mati" + }, "plugwise__regulation_mode": { "bleeding_cold": "Dingin sekali", "bleeding_hot": "Panas sekali", diff --git a/homeassistant/components/plugwise/translations/select.it.json b/homeassistant/components/plugwise/translations/select.it.json index 97106b6c5b9..3407312874e 100644 --- a/homeassistant/components/plugwise/translations/select.it.json +++ b/homeassistant/components/plugwise/translations/select.it.json @@ -1,10 +1,10 @@ { "state": { "plugwise__dhw_mode": { - "auto": "Automatico", + "auto": "Automatica", "boost": "Velocizza", "comfort": "Comfort", - "off": "Spento" + "off": "Spenta" }, "plugwise__regulation_mode": { "bleeding_cold": "Sfiatamento freddo", diff --git a/homeassistant/components/plugwise/translations/select.tr.json b/homeassistant/components/plugwise/translations/select.tr.json index 9ae8b443ebd..0941019ce07 100644 --- a/homeassistant/components/plugwise/translations/select.tr.json +++ b/homeassistant/components/plugwise/translations/select.tr.json @@ -1,5 +1,11 @@ { "state": { + "plugwise__dhw_mode": { + "auto": "Otomatik", + "boost": "G\u00fc\u00e7l\u00fc", + "comfort": "Konfor", + "off": "Kapal\u0131" + }, "plugwise__regulation_mode": { "bleeding_cold": "So\u011futma", "bleeding_hot": "Is\u0131tma", diff --git a/homeassistant/components/plugwise/translations/sk.json b/homeassistant/components/plugwise/translations/sk.json index 7124f1e5e28..12048de0987 100644 --- a/homeassistant/components/plugwise/translations/sk.json +++ b/homeassistant/components/plugwise/translations/sk.json @@ -1,13 +1,21 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", + "unsupported": "Zariadenie s nepodporovan\u00fdm firmv\u00e9rom" }, "step": { - "user_gateway": { + "user": { "data": { + "host": "IP adresa", "port": "Port" - } + }, + "description": "Pros\u00edm, zadajte" } } } diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index 379e1eb8a8c..6829648b9fb 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -10,11 +10,9 @@ "invalid_setup": "L\u00e4gg till din Adam ist\u00e4llet f\u00f6r din Anna, se Home Assistant Plugwise integrationsdokumentation f\u00f6r mer information", "unknown": "Ov\u00e4ntat fel" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Anslutningstyp", "host": "IP-adress", "password": "Smile ID", "port": "Port", @@ -22,26 +20,6 @@ }, "description": "Ange", "title": "Anslut till leendet" - }, - "user_gateway": { - "data": { - "host": "IP address", - "password": "Smile ID", - "port": "Port", - "username": "Smile Anv\u00e4ndarnamn" - }, - "description": "V\u00e4nligen ange:", - "title": "Anslut till leendet" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Skanningsintervall (sekunder)" - }, - "description": "Justera Plugwise-alternativ" } } } diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index 41f52761dbf..fb14d8da2d8 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -10,11 +10,9 @@ "invalid_setup": "Anna'n\u0131z yerine Adam'\u0131n\u0131z\u0131 ekleyin, daha fazla bilgi i\u00e7in Home Assistant Plugwise entegrasyon belgelerine bak\u0131n", "unknown": "Beklenmeyen hata" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Ba\u011flant\u0131 t\u00fcr\u00fc", "host": "IP Adresi", "password": "Smile Kimli\u011fi", "port": "Port", @@ -22,26 +20,6 @@ }, "description": "L\u00fctfen girin", "title": "Smile'a Ba\u011flan\u0131n" - }, - "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", - "title": "Smile'a Ba\u011flan\u0131n" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)" - }, - "description": "Plugwise Se\u00e7eneklerini Ayarlay\u0131n" } } } diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json index 6c6f54612b1..ac62753459b 100644 --- a/homeassistant/components/plugwise/translations/uk.json +++ b/homeassistant/components/plugwise/translations/uk.json @@ -8,34 +8,10 @@ "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" } } } diff --git a/homeassistant/components/plugwise/translations/zh-Hans.json b/homeassistant/components/plugwise/translations/zh-Hans.json index 80c129af78d..b8ed72ea7af 100644 --- a/homeassistant/components/plugwise/translations/zh-Hans.json +++ b/homeassistant/components/plugwise/translations/zh-Hans.json @@ -1,12 +1,8 @@ { "config": { - "step": { - "user_gateway": { - "data": { - "host": "IP\u5730\u5740", - "port": "\u7aef\u53e3" - } - } + "error": { + "response_error": "\u65e0\u6548\u7684 XML \u6570\u636e\uff0c\u6216\u6536\u5230\u7684\u9519\u8bef\u6307\u793a", + "unsupported": "\u8bbe\u5907\u5b89\u88c5\u4e86\u4e0d\u88ab\u652f\u6301\u7684\u56fa\u4ef6" } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 2ea2c4b09ba..07ebe6e64ef 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "invalid_setup": "\u65b0\u589e Adam \u800c\u975e Anna\u3001\u8acb\u53c3\u95b1 Home Assistant Plugwise \u6574\u5408\u8aaa\u660e\u6587\u4ef6\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "invalid_setup": "\u65b0\u589e Adam \u800c\u975e Anna\u3001\u8acb\u53c3\u95b1\u8aaa\u660e\u6587\u4ef6", + "response_error": "XML \u8cc7\u6599\u7121\u6548\u3001\u6216\u63a5\u6536\u5230\u932f\u8aa4\u6307\u793a", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unsupported": "\u88dd\u7f6e\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "\u9023\u7dda\u985e\u5225", "host": "IP \u4f4d\u5740", "password": "Smile ID", "port": "\u901a\u8a0a\u57e0", @@ -22,26 +22,6 @@ }, "description": "\u8acb\u8f38\u5165", "title": "\u9023\u7dda\u81f3 Smile" - }, - "user_gateway": { - "data": { - "host": "IP \u4f4d\u5740", - "password": "Smile ID", - "port": "\u901a\u8a0a\u57e0", - "username": "Smile \u4f7f\u7528\u8005\u540d\u7a31" - }, - "description": "\u8acb\u8f38\u5165\uff1a", - "title": "\u9023\u7dda\u81f3 Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09" - }, - "description": "\u8abf\u6574 Plugwise \u9078\u9805" } } } diff --git a/homeassistant/components/plum_lightpad/translations/sk.json b/homeassistant/components/plum_lightpad/translations/sk.json index ee5407aae19..a89b9b15d54 100644 --- a/homeassistant/components/plum_lightpad/translations/sk.json +++ b/homeassistant/components/plum_lightpad/translations/sk.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { + "password": "Heslo", "username": "Email" } } diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index eba495a6c61..619180c0460 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -22,8 +22,6 @@ from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW _LOGGER = logging.getLogger(__name__) -DEVICE_CLASS_SOUND = "sound_level" - @dataclass class MinutPointRequiredKeysMixin: @@ -55,7 +53,6 @@ SENSOR_TYPES: tuple[MinutPointSensorEntityDescription, ...] = ( MinutPointSensorEntityDescription( key="sound", precision=1, - device_class=DEVICE_CLASS_SOUND, icon="mdi:ear-hearing", native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, ), diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index e902da5ab4c..ff9a3bf6c7b 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", + "external_setup": "Point erfolgreich von einem anderen Flow konfiguriert.", "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" }, diff --git a/homeassistant/components/point/translations/sk.json b/homeassistant/components/point/translations/sk.json index c19b1a0b70c..e650ef4e01f 100644 --- a/homeassistant/components/point/translations/sk.json +++ b/homeassistant/components/point/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_setup": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "unknown_authorize_url_generation": "Nezn\u00e1ma chyba pri generovan\u00ed autorizovanej adresy URL." + }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "error": { + "no_token": "Neplatn\u00fd pr\u00edstupov\u00fd token" + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?", + "title": "Vyberte met\u00f3du overenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/sk.json b/homeassistant/components/poolsense/translations/sk.json index 72b0304f1c3..bfca61034ec 100644 --- a/homeassistant/components/poolsense/translations/sk.json +++ b/homeassistant/components/poolsense/translations/sk.json @@ -1,12 +1,16 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" } } } diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 3d1eb07f3fa..d5baae17df6 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -8,6 +8,9 @@ "dhcp": [ { "hostname": "1118431-*" + }, + { + "hostname": "1232100-*" } ], "iot_class": "local_polling", diff --git a/homeassistant/components/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json index f0092b14bc1..5660ffe8a10 100644 --- a/homeassistant/components/powerwall/translations/bg.json +++ b/homeassistant/components/powerwall/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { diff --git a/homeassistant/components/powerwall/translations/sk.json b/homeassistant/components/powerwall/translations/sk.json index 71a7aea5018..ae43b2fffa0 100644 --- a/homeassistant/components/powerwall/translations/sk.json +++ b/homeassistant/components/powerwall/translations/sk.json @@ -1,10 +1,30 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "flow_title": "{name} ({ip_address})", + "step": { + "confirm_discovery": { + "description": "Chcete nastavi\u0165 {name} ({ip_address})?" + }, + "reauth_confim": { + "data": { + "password": "Heslo" + } + }, + "user": { + "data": { + "ip_address": "IP adresa", + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/sk.json b/homeassistant/components/profiler/translations/sk.json new file mode 100644 index 00000000000..6ba11236f08 --- /dev/null +++ b/homeassistant/components/profiler/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/sk.json b/homeassistant/components/progettihwsw/translations/sk.json index 892b8b2cd91..1ad2241b9e5 100644 --- a/homeassistant/components/progettihwsw/translations/sk.json +++ b/homeassistant/components/progettihwsw/translations/sk.json @@ -1,8 +1,36 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e9 1", + "relay_10": "Rel\u00e9 10", + "relay_11": "Rel\u00e9 11", + "relay_12": "Rel\u00e9 12", + "relay_13": "Rel\u00e9 13", + "relay_14": "Rel\u00e9 14", + "relay_15": "Rel\u00e9 15", + "relay_16": "Rel\u00e9 16", + "relay_2": "Rel\u00e9 2", + "relay_3": "Rel\u00e9 3", + "relay_4": "Rel\u00e9 4", + "relay_5": "Rel\u00e9 5", + "relay_6": "Rel\u00e9 6", + "relay_7": "Rel\u00e9 7", + "relay_8": "Rel\u00e9 8", + "relay_9": "Rel\u00e9 9" + } + }, "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/prosegur/translations/bg.json b/homeassistant/components/prosegur/translations/bg.json index c00de2c8049..48c3f434084 100644 --- a/homeassistant/components/prosegur/translations/bg.json +++ b/homeassistant/components/prosegur/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/prosegur/translations/sk.json b/homeassistant/components/prosegur/translations/sk.json index 71a7aea5018..7744a00340e 100644 --- a/homeassistant/components/prosegur/translations/sk.json +++ b/homeassistant/components/prosegur/translations/sk.json @@ -1,10 +1,28 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "country": "Krajina", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 59d19bc785e..e863122b872 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==9.2.0"], + "requirements": ["pillow==9.3.0"], "codeowners": [] } diff --git a/homeassistant/components/prusalink/translations/sensor.sk.json b/homeassistant/components/prusalink/translations/sensor.sk.json new file mode 100644 index 00000000000..d35150afe2e --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.sk.json @@ -0,0 +1,8 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Prebieha ru\u0161enie", + "printing": "Tla\u010d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sk.json b/homeassistant/components/prusalink/translations/sk.json new file mode 100644 index 00000000000..1ae350ddfd3 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "not_supported": "Podporovan\u00e9 je iba PrusaLink API v2", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 5202825c85f..b5421af279b 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -67,6 +67,7 @@ async def async_setup_entry( class PS4Device(MediaPlayerEntity): """Representation of a PS4.""" + _attr_icon = ICON _attr_supported_features = ( MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_ON @@ -80,20 +81,13 @@ class PS4Device(MediaPlayerEntity): self._entry_id = config.entry_id self._ps4 = ps4 self._host = host - self._name = name + self._attr_name = name self._region = region self._creds = creds - self._state = None - self._media_content_id = None - self._media_title = None self._media_image = None - self._media_type = None - self._source = None self._games = {} - self._source_list = [] self._retry = 0 self._disconnected = False - self._unique_id = None @callback def status_callback(self): @@ -161,7 +155,7 @@ class PS4Device(MediaPlayerEntity): def _parse_status(self): """Parse status.""" if (status := self._ps4.status) is not None: - self._games = load_games(self.hass, self._unique_id) + self._games = load_games(self.hass, self.unique_id) if self._games: self.get_source_list() @@ -172,24 +166,24 @@ class PS4Device(MediaPlayerEntity): name = status.get("running-app-name") if title_id and name is not None: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING - if self._media_content_id != title_id: - self._media_content_id = title_id + if self.media_content_id != title_id: + self._attr_media_content_id = title_id if self._use_saved(): _LOGGER.debug("Using saved data for media: %s", title_id) return - self._media_title = name - self._source = self._media_title - self._media_type = None + self._attr_media_title = name + self._attr_source = self._attr_media_title + self._attr_media_content_type = None # Get data from PS Store. asyncio.ensure_future(self.async_get_title_data(title_id, name)) else: - if self._state != MediaPlayerState.IDLE: + if self.state != MediaPlayerState.IDLE: self.idle() else: - if self._state != MediaPlayerState.STANDBY: + if self.state != MediaPlayerState.STANDBY: self.state_standby() elif self._retry > DEFAULT_RETRIES: @@ -199,32 +193,32 @@ class PS4Device(MediaPlayerEntity): def _use_saved(self) -> bool: """Return True, Set media attrs if data is locked.""" - if self._media_content_id in self._games: - store = self._games[self._media_content_id] + if self.media_content_id in self._games: + store = self._games[self.media_content_id] # If locked get attributes from file. if store.get(ATTR_LOCKED): - self._media_title = store.get(ATTR_MEDIA_TITLE) - self._source = self._media_title + self._attr_media_title = store.get(ATTR_MEDIA_TITLE) + self._attr_source = self._attr_media_title self._media_image = store.get(ATTR_MEDIA_IMAGE_URL) - self._media_type = store.get(ATTR_MEDIA_CONTENT_TYPE) + self._attr_media_content_type = store.get(ATTR_MEDIA_CONTENT_TYPE) return True return False def idle(self): """Set states for state idle.""" self.reset_title() - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE def state_standby(self): """Set states for state standby.""" self.reset_title() - self._state = MediaPlayerState.STANDBY + self._attr_state = MediaPlayerState.STANDBY def state_unknown(self): """Set states for state unknown.""" self.reset_title() - self._state = None + self._attr_state = None if self._disconnected is False: _LOGGER.warning("PS4 could not be reached") self._disconnected = True @@ -232,10 +226,10 @@ class PS4Device(MediaPlayerEntity): def reset_title(self): """Update if there is no title.""" - self._media_title = None - self._media_content_id = None - self._media_type = None - self._source = None + self._attr_media_title = None + self._attr_media_content_id = None + self._attr_media_content_type = None + self._attr_source = None async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" @@ -271,42 +265,42 @@ class PS4Device(MediaPlayerEntity): ) finally: - self._media_title = app_name or name - self._source = self._media_title + self._attr_media_title = app_name or name + self._attr_source = self._attr_media_title self._media_image = art or None - self._media_type = media_type + self._attr_media_content_type = media_type await self.hass.async_add_executor_job(self.update_list) self.async_write_ha_state() def update_list(self): """Update Game List, Correct data if different.""" - if self._media_content_id in self._games: - store = self._games[self._media_content_id] + if self.media_content_id in self._games: + store = self._games[self.media_content_id] if ( - store.get(ATTR_MEDIA_TITLE) != self._media_title + store.get(ATTR_MEDIA_TITLE) != self.media_title or store.get(ATTR_MEDIA_IMAGE_URL) != self._media_image ): - self._games.pop(self._media_content_id) + self._games.pop(self.media_content_id) - if self._media_content_id not in self._games: + if self.media_content_id not in self._games: self.add_games( - self._media_content_id, - self._media_title, + self.media_content_id, + self._attr_media_title, self._media_image, - self._media_type, + self._attr_media_content_type, ) - self._games = load_games(self.hass, self._unique_id) + self._games = load_games(self.hass, self.unique_id) self.get_source_list() - def get_source_list(self): + def get_source_list(self) -> None: """Parse data entry and update source list.""" games = [] for data in self._games.values(): games.append(data[ATTR_MEDIA_TITLE]) - self._source_list = sorted(games) + self._attr_source_list = sorted(games) def add_games(self, title_id, app_name, image, g_type, is_locked=False): """Add games to list.""" @@ -321,7 +315,7 @@ class PS4Device(MediaPlayerEntity): } } games.update(game) - save_games(self.hass, games, self._unique_id) + save_games(self.hass, games, self.unique_id) async def async_get_device_info(self, status): """Set device info for registry.""" @@ -332,7 +326,7 @@ class PS4Device(MediaPlayerEntity): d_registry = device_registry.async_get(self.hass) for entity_id, entry in e_registry.entities.items(): if entry.config_entry_id == self._entry_id: - self._unique_id = entry.unique_id + self._attr_unique_id = entry.unique_id self.entity_id = entity_id break for device in d_registry.devices.values(): @@ -358,7 +352,7 @@ class PS4Device(MediaPlayerEntity): sw_version=sw_version, ) - self._unique_id = format_unique_id(self._creds, status["host-id"]) + self._attr_unique_id = format_unique_id(self._creds, status["host-id"]) async def async_will_remove_from_hass(self) -> None: """Remove Entity from Home Assistant.""" @@ -368,17 +362,12 @@ class PS4Device(MediaPlayerEntity): self.unsubscribe_to_protocol() self.hass.data[PS4_DATA].devices.remove(self) - @property - def unique_id(self): - """Return Unique ID for entity.""" - return self._unique_id - @property def entity_picture(self): """Return picture.""" if ( - self._state == MediaPlayerState.PLAYING - and self._media_content_id is not None + self.state == MediaPlayerState.PLAYING + and self.media_content_id is not None and (image_hash := self.media_image_hash) is not None ): return ( @@ -387,53 +376,13 @@ class PS4Device(MediaPlayerEntity): ) return MEDIA_IMAGE_DEFAULT - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def icon(self): - """Icon.""" - return ICON - - @property - def media_content_id(self): - """Content ID of current playing media.""" - return self._media_content_id - - @property - def media_content_type(self): - """Content type of current playing media.""" - return self._media_type - @property def media_image_url(self): """Image url of current playing media.""" - if self._media_content_id is None: + if self.media_content_id is None: return MEDIA_IMAGE_DEFAULT return self._media_image - @property - def media_title(self): - """Title of current playing media.""" - return self._media_title - - @property - def source(self): - """Return the current input source.""" - return self._source - - @property - def source_list(self): - """List of available input sources.""" - return self._source_list - async def async_turn_off(self) -> None: """Turn off media player.""" await self._ps4.standby() @@ -468,7 +417,7 @@ class PS4Device(MediaPlayerEntity): "Starting PS4 game %s (%s) using source %s", game, title_id, source ) - await self._ps4.start_title(title_id, self._media_content_id) + await self._ps4.start_title(title_id, self.media_content_id) return _LOGGER.warning("Could not start title. '%s' is not in source list", source) diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json index 0aaba028b7e..fe4f5d7b8d2 100644 --- a/homeassistant/components/ps4/translations/he.json +++ b/homeassistant/components/ps4/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/ps4/translations/sk.json b/homeassistant/components/ps4/translations/sk.json index fa12207330b..bb9cdddbaac 100644 --- a/homeassistant/components/ps4/translations/sk.json +++ b/homeassistant/components/ps4/translations/sk.json @@ -1,13 +1,32 @@ { "config": { "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "port_987_bind_error": "Nepodarilo sa naviaza\u0165 na port 987. \u010eal\u0161ie inform\u00e1cie n\u00e1jdete v [dokument\u00e1cii](https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "Nepodarilo sa naviaza\u0165 na port 997. \u010eal\u0161ie inform\u00e1cie n\u00e1jdete v [dokument\u00e1cii](https://www.home-assistant.io/components/ps4/)." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "credential_timeout": "\u010casov\u00fd limit slu\u017eby poveren\u00ed vypr\u0161al. Re\u0161tartujte stla\u010den\u00edm tla\u010didla Odosla\u0165.", + "login_failed": "Nepodarilo sa sp\u00e1rova\u0165 s PlayStation 4. Overte, \u010di je PIN k\u00f3d spr\u00e1vny.", + "no_ipaddress": "Zadajte IP adresa konzoly PlayStation 4, ktor\u00fa chcete nakonfigurova\u0165." }, "step": { "link": { "data": { + "code": "PIN k\u00f3d", + "ip_address": "IP adresa", "name": "N\u00e1zov" } + }, + "mode": { + "data": { + "ip_address": "IP adresa (Ak pou\u017e\u00edvate automatick\u00e9 zis\u0165ovanie, nechajte pr\u00e1zdne)." + }, + "data_description": { + "ip_address": "Ak vyberiete automatick\u00e9 zis\u0165ovanie, ponechajte pr\u00e1zdne." + } } } } diff --git a/homeassistant/components/pure_energie/translations/it.json b/homeassistant/components/pure_energie/translations/it.json index f3b7419fc1d..ebef5a12c99 100644 --- a/homeassistant/components/pure_energie/translations/it.json +++ b/homeassistant/components/pure_energie/translations/it.json @@ -19,7 +19,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere Pure Energie Meter (`{model}`) a Home Assistant?", - "title": "Scoperto dispositivo Pure Energie Meter" + "title": "Rilevato dispositivo Pure Energie Meter" } } } diff --git a/homeassistant/components/pure_energie/translations/ru.json b/homeassistant/components/pure_energie/translations/ru.json index 2aa39757104..e2f7bf36745 100644 --- a/homeassistant/components/pure_energie/translations/ru.json +++ b/homeassistant/components/pure_energie/translations/ru.json @@ -14,7 +14,7 @@ "host": "\u0425\u043e\u0441\u0442" }, "data_description": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0433\u043e Pure Energie Meter." + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0433\u043e Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/sk.json b/homeassistant/components/pure_energie/translations/sk.json new file mode 100644 index 00000000000..42e2c99e2f3 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + }, + "data_description": { + "host": "IP adresa alebo n\u00e1zov hostite\u013ea v\u00e1\u0161ho mera\u010da energie Pure." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/__init__.py b/homeassistant/components/pushbullet/__init__.py index 153fa389fcc..bed0e94ccd9 100644 --- a/homeassistant/components/pushbullet/__init__.py +++ b/homeassistant/components/pushbullet/__init__.py @@ -1 +1,79 @@ """The pushbullet component.""" +from __future__ import annotations + +import logging + +from pushbullet import InvalidKeyError, PushBullet, PushbulletError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + Platform, +) +from homeassistant.core import Event, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType + +from .api import PushBulletNotificationProvider +from .const import DATA_HASS_CONFIG, DOMAIN + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the pushbullet component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up pushbullet from a config entry.""" + + try: + pushbullet = await hass.async_add_executor_job( + PushBullet, entry.data[CONF_API_KEY] + ) + except InvalidKeyError: + _LOGGER.error("Invalid API key for Pushbullet") + return False + except PushbulletError as err: + raise ConfigEntryNotReady from err + + pb_provider = PushBulletNotificationProvider(hass, pushbullet) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = pb_provider + + def start_listener(event: Event) -> None: + """Start the listener thread.""" + _LOGGER.debug("Starting listener for pushbullet") + pb_provider.start() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_listener) + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_NAME: entry.data[CONF_NAME], "entry_id": entry.entry_id}, + hass.data[DATA_HASS_CONFIG], + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + pb_provider: PushBulletNotificationProvider = hass.data[DOMAIN].pop( + entry.entry_id + ) + await hass.async_add_executor_job(pb_provider.close) + return unload_ok diff --git a/homeassistant/components/pushbullet/api.py b/homeassistant/components/pushbullet/api.py new file mode 100644 index 00000000000..ff6a57aa931 --- /dev/null +++ b/homeassistant/components/pushbullet/api.py @@ -0,0 +1,32 @@ +"""Pushbullet Notification provider.""" + +from typing import Any + +from pushbullet import Listener, PushBullet + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import DATA_UPDATED + + +class PushBulletNotificationProvider(Listener): + """Provider for an account, leading to one or more sensors.""" + + def __init__(self, hass: HomeAssistant, pushbullet: PushBullet) -> None: + """Start to retrieve pushes from the given Pushbullet instance.""" + self.hass = hass + self.pushbullet = pushbullet + self.data: dict[str, Any] = {} + super().__init__(account=pushbullet, on_push=self.update_data) + self.daemon = True + + def update_data(self, data: dict[str, Any]) -> None: + """Update the current data. + + Currently only monitors pushes but might be extended to monitor + different kinds of Pushbullet events. + """ + if data["type"] == "push": + self.data = data["push"] + dispatcher_send(self.hass, DATA_UPDATED) diff --git a/homeassistant/components/pushbullet/config_flow.py b/homeassistant/components/pushbullet/config_flow.py new file mode 100644 index 00000000000..bfa12a911b6 --- /dev/null +++ b/homeassistant/components/pushbullet/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for pushbullet integration.""" +from __future__ import annotations + +from typing import Any + +from pushbullet import InvalidKeyError, PushBullet, PushbulletError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector + +from .const import DEFAULT_NAME, DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): selector.TextSelector(), + vol.Required(CONF_API_KEY): selector.TextSelector(), + } +) + + +class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for pushbullet integration.""" + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Handle import from config.""" + import_config[CONF_NAME] = import_config.get(CONF_NAME, DEFAULT_NAME) + return await self.async_step_user(import_config) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + + self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) + + try: + pushbullet = await self.hass.async_add_executor_job( + PushBullet, user_input[CONF_API_KEY] + ) + except InvalidKeyError: + errors[CONF_API_KEY] = "invalid_api_key" + except PushbulletError: + errors["base"] = "cannot_connect" + + if not errors: + await self.async_set_unique_id(pushbullet.user_info["iden"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=CONFIG_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/pushbullet/const.py b/homeassistant/components/pushbullet/const.py new file mode 100644 index 00000000000..de81f56e862 --- /dev/null +++ b/homeassistant/components/pushbullet/const.py @@ -0,0 +1,12 @@ +"""Constants for the pushbullet integration.""" + +from typing import Final + +DOMAIN: Final = "pushbullet" +DEFAULT_NAME: Final = "Pushbullet" +DATA_HASS_CONFIG: Final = "pushbullet_hass_config" +DATA_UPDATED: Final = "pushbullet_data_updated" + +ATTR_URL: Final = "url" +ATTR_FILE: Final = "file" +ATTR_FILE_URL: Final = "file_url" diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 7931cca70cc..7fcaa59fbb8 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -3,7 +3,8 @@ "name": "Pushbullet", "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": ["pushbullet.py==0.11.0"], - "codeowners": [], + "codeowners": ["@engrbm87"], + "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pushbullet"] } diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 6f851f8000e..fcc9d00dc7a 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -1,8 +1,13 @@ """Pushbullet platform for notify component.""" +from __future__ import annotations + import logging import mimetypes +from typing import Any -from pushbullet import InvalidKeyError, PushBullet, PushError +from pushbullet import PushBullet, PushError +from pushbullet.channel import Channel +from pushbullet.device import Device import voluptuous as vol from homeassistant.components.notify import ( @@ -13,59 +18,69 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import ATTR_FILE, ATTR_FILE_URL, ATTR_URL, DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_URL = "url" -ATTR_FILE = "file" -ATTR_FILE_URL = "file_url" -ATTR_LIST = "list" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -def get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> PushBulletNotificationService | None: """Get the Pushbullet notification service.""" - - try: - pushbullet = PushBullet(config[CONF_API_KEY]) - except InvalidKeyError: - _LOGGER.error("Wrong API key supplied") + if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.2.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) return None - return PushBulletNotificationService(pushbullet) + pushbullet: PushBullet = hass.data[DOMAIN][discovery_info["entry_id"]].pushbullet + return PushBulletNotificationService(hass, pushbullet) class PushBulletNotificationService(BaseNotificationService): """Implement the notification service for Pushbullet.""" - def __init__(self, pb): # pylint: disable=invalid-name + def __init__(self, hass: HomeAssistant, pushbullet: PushBullet) -> None: """Initialize the service.""" - self.pushbullet = pb - self.pbtargets = {} - self.refresh() + self.hass = hass + self.pushbullet = pushbullet - def refresh(self): - """Refresh devices, contacts, etc. - - pbtargets stores all targets available from this Pushbullet instance - into a dict. These are Pushbullet objects!. It sacrifices a bit of - memory for faster processing at send_message. - - As of sept 2015, contacts were replaced by chats. This is not - implemented in the module yet. - """ - self.pushbullet.refresh() - self.pbtargets = { + @property + def pbtargets(self) -> dict[str, dict[str, Device | Channel]]: + """Return device and channel detected targets.""" + return { "device": {tgt.nickname.lower(): tgt for tgt in self.pushbullet.devices}, "channel": { tgt.channel_tag.lower(): tgt for tgt in self.pushbullet.channels }, } - def send_message(self, message=None, **kwargs): + def send_message(self, message: str, **kwargs: Any) -> None: """Send a message to a specified target. If no target specified, a 'normal' push will be sent to all devices @@ -73,24 +88,25 @@ class PushBulletNotificationService(BaseNotificationService): Email is special, these are assumed to always exist. We use a special call which doesn't require a push object. """ - targets = kwargs.get(ATTR_TARGET) - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - data = kwargs.get(ATTR_DATA) - refreshed = False + targets: list[str] = kwargs.get(ATTR_TARGET, []) + title: str = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + data: dict[str, Any] = kwargs[ATTR_DATA] or {} if not targets: # Backward compatibility, notify all devices in own account. self._push_data(message, title, data, self.pushbullet) - _LOGGER.info("Sent notification to self") + _LOGGER.debug("Sent notification to self") return + # refresh device and channel targets + self.pushbullet.refresh() + # Main loop, process all targets specified. for target in targets: try: ttype, tname = target.split("/", 1) - except ValueError: - _LOGGER.error("Invalid target syntax: %s", target) - continue + except ValueError as err: + raise ValueError(f"Invalid target syntax: '{target}'") from err # Target is email, send directly, don't use a target object. # This also seems to work to send to all devices in own account. @@ -107,71 +123,57 @@ class PushBulletNotificationService(BaseNotificationService): _LOGGER.info("Sent sms notification to %s", tname) continue - # Refresh if name not found. While awaiting periodic refresh - # solution in component, poor mans refresh. if ttype not in self.pbtargets: - _LOGGER.error("Invalid target syntax: %s", target) - continue + raise ValueError(f"Invalid target syntax: {target}") tname = tname.lower() - if tname not in self.pbtargets[ttype] and not refreshed: - self.refresh() - refreshed = True + if tname not in self.pbtargets[ttype]: + raise ValueError(f"Target: {target} doesn't exist") # Attempt push_note on a dict value. Keys are types & target # name. Dict pbtargets has all *actual* targets. - try: - self._push_data(message, title, data, self.pbtargets[ttype][tname]) - _LOGGER.info("Sent notification to %s/%s", ttype, tname) - except KeyError: - _LOGGER.error("No such target: %s/%s", ttype, tname) - continue + self._push_data(message, title, data, self.pbtargets[ttype][tname]) + _LOGGER.debug("Sent notification to %s/%s", ttype, tname) - def _push_data(self, message, title, data, pusher, email=None, phonenumber=None): + def _push_data( + self, + message: str, + title: str, + data: dict[str, Any], + pusher: PushBullet, + email: str | None = None, + phonenumber: str | None = None, + ): """Create the message content.""" + kwargs = {"body": message, "title": title} + if email: + kwargs["email"] = email - if data is None: - data = {} - data_list = data.get(ATTR_LIST) - url = data.get(ATTR_URL) - filepath = data.get(ATTR_FILE) - file_url = data.get(ATTR_FILE_URL) try: - email_kwargs = {} - if email: - email_kwargs["email"] = email - if phonenumber: - device = pusher.devices[0] - pusher.push_sms(device, phonenumber, message) - elif url: - pusher.push_link(title, url, body=message, **email_kwargs) - elif filepath: + if phonenumber and pusher.devices: + pusher.push_sms(pusher.devices[0], phonenumber, message) + return + if url := data.get(ATTR_URL): + pusher.push_link(url=url, **kwargs) + return + if filepath := data.get(ATTR_FILE): if not self.hass.config.is_allowed_path(filepath): - _LOGGER.error("Filepath is not valid or allowed") - return + raise ValueError("Filepath is not valid or allowed") with open(filepath, "rb") as fileh: filedata = self.pushbullet.upload_file(fileh, filepath) - if filedata.get("file_type") == "application/x-empty": - _LOGGER.error("Can not send an empty file") - return - filedata.update(email_kwargs) - pusher.push_file(title=title, body=message, **filedata) - elif file_url: - if not file_url.startswith("http"): - _LOGGER.error("URL should start with http or https") - return + if filedata.get("file_type") == "application/x-empty": + raise ValueError("Cannot send an empty file") + kwargs.update(filedata) + pusher.push_file(**kwargs) + elif (file_url := data.get(ATTR_FILE_URL)) and vol.Url(file_url): pusher.push_file( - title=title, - body=message, file_name=file_url, file_url=file_url, file_type=(mimetypes.guess_type(file_url)[0]), - **email_kwargs, + **kwargs, ) - elif data_list: - pusher.push_list(title, data_list, **email_kwargs) else: - pusher.push_note(title, message, **email_kwargs) + pusher.push_note(**kwargs) except PushError as err: - _LOGGER.error("Notify failed: %s", err) + raise HomeAssistantError(f"Notify failed: {err}") from err diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 51a18f1aaea..aef97991c66 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -1,10 +1,6 @@ """Pushbullet platform for sensor component.""" from __future__ import annotations -import logging -import threading - -from pushbullet import InvalidKeyError, Listener, PushBullet import voluptuous as vol from homeassistant.components.sensor import ( @@ -12,18 +8,25 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -_LOGGER = logging.getLogger(__name__) +from .api import PushBulletNotificationProvider +from .const import DATA_UPDATED, DOMAIN SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="application_name", name="Application name", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="body", @@ -32,26 +35,32 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="notification_id", name="Notification ID", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="notification_tag", name="Notification tag", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="package_name", name="Package name", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="receiver_email", name="Receiver email", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="sender_email", name="Sender email", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="source_device_iden", name="Sender device ID", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="title", @@ -60,6 +69,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="type", name="Type", + entity_registry_enabled_default=False, ), ) @@ -75,94 +85,88 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Pushbullet Sensor platform.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.2.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - try: - pushbullet = PushBullet(config.get(CONF_API_KEY)) - except InvalidKeyError: - _LOGGER.error("Wrong API key for Pushbullet supplied") - return - pbprovider = PushBulletNotificationProvider(pushbullet) +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Pushbullet sensors from config entry.""" + + pb_provider: PushBulletNotificationProvider = hass.data[DOMAIN][entry.entry_id] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] entities = [ - PushBulletNotificationSensor(pbprovider, description) + PushBulletNotificationSensor(entry.data[CONF_NAME], pb_provider, description) for description in SENSOR_TYPES - if description.key in monitored_conditions ] - add_entities(entities) + + async_add_entities(entities) class PushBulletNotificationSensor(SensorEntity): """Representation of a Pushbullet Sensor.""" + _attr_should_poll = False + _attr_has_entity_name = True + def __init__( self, - pb, # pylint: disable=invalid-name + name: str, + pb_provider: PushBulletNotificationProvider, description: SensorEntityDescription, - ): + ) -> None: """Initialize the Pushbullet sensor.""" self.entity_description = description - self.pushbullet = pb + self.pb_provider = pb_provider + self._attr_unique_id = ( + f"{pb_provider.pushbullet.user_info['iden']}-{description.key}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, pb_provider.pushbullet.user_info["iden"])}, + name=name, + entry_type=DeviceEntryType.SERVICE, + ) - self._attr_name = f"Pushbullet {description.key}" - - def update(self) -> None: + @callback + def async_update_callback(self) -> None: """Fetch the latest data from the sensor. This will fetch the 'sensor reading' into self._state but also all attributes into self._state_attributes. """ try: - self._attr_native_value = self.pushbullet.data[self.entity_description.key] - self._attr_extra_state_attributes = self.pushbullet.data + self._attr_native_value = self.pb_provider.data[self.entity_description.key] + self._attr_extra_state_attributes = self.pb_provider.data except (KeyError, TypeError): pass + self.async_write_ha_state() - -class PushBulletNotificationProvider: - """Provider for an account, leading to one or more sensors.""" - - def __init__(self, pushbullet): - """Start to retrieve pushes from the given Pushbullet instance.""" - - self.pushbullet = pushbullet - self._data = None - self.listener = None - self.thread = threading.Thread(target=self.retrieve_pushes) - self.thread.daemon = True - self.thread.start() - - def on_push(self, data): - """Update the current data. - - Currently only monitors pushes but might be extended to monitor - different kinds of Pushbullet events. - """ - if data["type"] == "push": - self._data = data["push"] - - @property - def data(self): - """Return the current data stored in the provider.""" - return self._data - - def retrieve_pushes(self): - """Retrieve_pushes. - - Spawn a new Listener and links it to self.on_push. - """ - - self.listener = Listener(account=self.pushbullet, on_push=self.on_push) - _LOGGER.debug("Getting pushes") - try: - self.listener.run_forever() - finally: - self.listener.close() + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, DATA_UPDATED, self.async_update_callback + ) + ) diff --git a/homeassistant/components/pushbullet/strings.json b/homeassistant/components/pushbullet/strings.json new file mode 100644 index 00000000000..92d22d117dc --- /dev/null +++ b/homeassistant/components/pushbullet/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Pushbullet YAML configuration is being removed", + "description": "Configuring Pushbullet using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushbullet YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/pushbullet/translations/bg.json b/homeassistant/components/pushbullet/translations/bg.json new file mode 100644 index 00000000000..11cf3c2b1ed --- /dev/null +++ b/homeassistant/components/pushbullet/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "name": "\u0418\u043c\u0435" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Pushbullet \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Pushbullet \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Pushbullet \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/ca.json b/homeassistant/components/pushbullet/translations/ca.json new file mode 100644 index 00000000000..a9ed110f004 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "name": "Nom" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Pushbullet mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Pushbullet del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Pushbullet est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/cs.json b/homeassistant/components/pushbullet/translations/cs.json new file mode 100644 index 00000000000..9b4579440a4 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/cs.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "name": "Jm\u00e9no" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurace Pushbullet pomoc\u00ed YAML se odstra\u0148uje. \n\nVa\u0161e st\u00e1vaj\u00edc\u00ed konfigurace YAML byla importov\u00e1na do u\u017eivatelsk\u00e9ho rozhran\u00ed automaticky. \n\nOdstra\u0148te konfiguraci Pushbullet YAML ze souboru configuration.yaml a restartujte Home Assistant, abyste tento probl\u00e9m vy\u0159e\u0161ili.", + "title": "Konfigurace Pushbullet YAML se odstra\u0148uje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/de.json b/homeassistant/components/pushbullet/translations/de.json new file mode 100644 index 00000000000..31679134a31 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "name": "Name" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Pushbullet mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Pushbullet-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Pushbullet YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/el.json b/homeassistant/components/pushbullet/translations/el.json new file mode 100644 index 00000000000..0f47d7be366 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Pushbullet \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushbullet YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushbullet YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/en.json b/homeassistant/components/pushbullet/translations/en.json new file mode 100644 index 00000000000..97175ddf0b0 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "name": "Name" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Pushbullet using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushbullet YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Pushbullet YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/es.json b/homeassistant/components/pushbullet/translations/es.json new file mode 100644 index 00000000000..4321936dbca --- /dev/null +++ b/homeassistant/components/pushbullet/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "name": "Nombre" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Pushbullet mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Pushbullet de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "La configuraci\u00f3n YAML de Pushbullet se va a eliminar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/et.json b/homeassistant/components/pushbullet/translations/et.json new file mode 100644 index 00000000000..85b090c2a47 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Kehtetu API v\u00f5ti" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "name": "Nimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Pushbulleti konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nSelle probleemi lahendamiseks eemalda failist configuration.yaml Pushbullet YAML konfiguratsioon ja taask\u00e4ivita Home Assistant.", + "title": "Pushbulleti YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/fr.json b/homeassistant/components/pushbullet/translations/fr.json new file mode 100644 index 00000000000..f17886f699e --- /dev/null +++ b/homeassistant/components/pushbullet/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/he.json b/homeassistant/components/pushbullet/translations/he.json new file mode 100644 index 00000000000..5f98f90ec8a --- /dev/null +++ b/homeassistant/components/pushbullet/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/hr.json b/homeassistant/components/pushbullet/translations/hr.json new file mode 100644 index 00000000000..01f02cb4496 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/hr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Servis je ve\u0107 konfiguriran" + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo", + "invalid_api_key": "Neispravan API klju\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API klju\u010d", + "name": "Ime" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "Pushbullet YAML konfiguracija se uklanja" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/hu.json b/homeassistant/components/pushbullet/translations/hu.json new file mode 100644 index 00000000000..09325e78e70 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "name": "Elnevez\u00e9s" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Pushbullet konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Pushbullet YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Pushbullet YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/id.json b/homeassistant/components/pushbullet/translations/id.json new file mode 100644 index 00000000000..6a5922193f9 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "name": "Nama" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi Pushbullet lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Pushbullet dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Pushbullet dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/it.json b/homeassistant/components/pushbullet/translations/it.json new file mode 100644 index 00000000000..94911243672 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "name": "Nome" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Pushbullet tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovere la configurazione YAML di Pushbullet dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Pushbullet \u00e8 stata rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/nl.json b/homeassistant/components/pushbullet/translations/nl.json new file mode 100644 index 00000000000..f50fd20c509 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "name": "Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/no.json b/homeassistant/components/pushbullet/translations/no.json new file mode 100644 index 00000000000..347ffdd05a5 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "name": "Navn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Pushbullet ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Pushbullet YAML-konfigurasjonen fra filen configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Pushbullet YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/pl.json b/homeassistant/components/pushbullet/translations/pl.json new file mode 100644 index 00000000000..eedc36d33e4 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "name": "Nazwa" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Pushbullet przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Pushbullet zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/pt-BR.json b/homeassistant/components/pushbullet/translations/pt-BR.json new file mode 100644 index 00000000000..7638ab3ca44 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "name": "Nome" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Pushbullet usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Pushbullet do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Pushbullet est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/ru.json b/homeassistant/components/pushbullet/translations/ru.json new file mode 100644 index 00000000000..815e732f7dd --- /dev/null +++ b/homeassistant/components/pushbullet/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \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_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushbullet \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushbullet \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/sk.json b/homeassistant/components/pushbullet/translations/sk.json new file mode 100644 index 00000000000..65dd8989032 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/zh-Hant.json b/homeassistant/components/pushbullet/translations/zh-Hant.json new file mode 100644 index 00000000000..2c641160218 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Pushbullet \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushbullet YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Pushbullet YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/bg.json b/homeassistant/components/pushover/translations/bg.json index 36e77da587c..49896debf18 100644 --- a/homeassistant/components/pushover/translations/bg.json +++ b/homeassistant/components/pushover/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Pushover \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Pushover \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Pushover \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/ca.json b/homeassistant/components/pushover/translations/ca.json index cbfc95c86ba..5a1beef3f9a 100644 --- a/homeassistant/components/pushover/translations/ca.json +++ b/homeassistant/components/pushover/translations/ca.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "La configuraci\u00f3 de Pushover mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Pushover del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "La configuraci\u00f3 YAML de Pushover est\u00e0 sent eliminada" - }, "removed_yaml": { "description": "La configuraci\u00f3 de Pushover mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML de Pushover del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Pushover s'ha eliminat" diff --git a/homeassistant/components/pushover/translations/cs.json b/homeassistant/components/pushover/translations/cs.json index 55a2608b01f..5435b116411 100644 --- a/homeassistant/components/pushover/translations/cs.json +++ b/homeassistant/components/pushover/translations/cs.json @@ -22,5 +22,10 @@ } } } + }, + "issues": { + "removed_yaml": { + "title": "Konfigurace Pushover YAML byla odstran\u011bna" + } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/de.json b/homeassistant/components/pushover/translations/de.json index 1a99ef663fa..3791b0981dd 100644 --- a/homeassistant/components/pushover/translations/de.json +++ b/homeassistant/components/pushover/translations/de.json @@ -26,13 +26,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Das Konfigurieren von Pushover mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Pushover-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Pushover-YAML-Konfiguration wird entfernt" - }, "removed_yaml": { "description": "Das Konfigurieren von Pushover mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die Pushover-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Pushover-YAML-Konfiguration wurde entfernt" + "title": "Die Pushover YAML-Konfiguration wurde entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/el.json b/homeassistant/components/pushover/translations/el.json index d79e3edd8f4..620383bfc67 100644 --- a/homeassistant/components/pushover/translations/el.json +++ b/homeassistant/components/pushover/translations/el.json @@ -26,9 +26,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Pushover \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushover YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushover YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Pushover \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushover YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Pushover YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/en.json b/homeassistant/components/pushover/translations/en.json index 4a126782dda..c16b5d285d0 100644 --- a/homeassistant/components/pushover/translations/en.json +++ b/homeassistant/components/pushover/translations/en.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Pushover YAML configuration is being removed" - }, "removed_yaml": { "description": "Configuring Pushover using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Pushover YAML configuration has been removed" diff --git a/homeassistant/components/pushover/translations/es.json b/homeassistant/components/pushover/translations/es.json index 418fe68c28e..e7958d06eee 100644 --- a/homeassistant/components/pushover/translations/es.json +++ b/homeassistant/components/pushover/translations/es.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de Pushover mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Pushover de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la configuraci\u00f3n YAML de Pushover" - }, "removed_yaml": { "description": "Se ha eliminado la configuraci\u00f3n de Pushover mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n Pushover YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Pushover" diff --git a/homeassistant/components/pushover/translations/et.json b/homeassistant/components/pushover/translations/et.json index 7ae9c0abbf1..28c721f19b9 100644 --- a/homeassistant/components/pushover/translations/et.json +++ b/homeassistant/components/pushover/translations/et.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Pushoveri seadistamine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage failist configuration.yaml-i pushover-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", - "title": "Pushover YAML konfiguratsioon eemaldatakse" - }, "removed_yaml": { "description": "Pushoveri konfigureerimine YAML-i abil on eemaldatud. \n\n Koduassistent ei kasuta teie olemasolevat YAML-i konfiguratsiooni. \n\n Selle probleemi lahendamiseks eemaldage Pushoveri YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", "title": "Pushoveri YAML-i konfiguratsioon on eemaldatud" diff --git a/homeassistant/components/pushover/translations/fr.json b/homeassistant/components/pushover/translations/fr.json index 2982b17064c..a8d7a213d64 100644 --- a/homeassistant/components/pushover/translations/fr.json +++ b/homeassistant/components/pushover/translations/fr.json @@ -24,10 +24,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "title": "La configuration YAML pour Pushover sera bient\u00f4t supprim\u00e9e" - } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/he.json b/homeassistant/components/pushover/translations/he.json index 9cdb8c5afcd..954ccefdde0 100644 --- a/homeassistant/components/pushover/translations/he.json +++ b/homeassistant/components/pushover/translations/he.json @@ -1,6 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/pushover/translations/hu.json b/homeassistant/components/pushover/translations/hu.json index cdb09291937..d517f037b3f 100644 --- a/homeassistant/components/pushover/translations/hu.json +++ b/homeassistant/components/pushover/translations/hu.json @@ -26,9 +26,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "A Pushover konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Pushover YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Pushover YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + "removed_yaml": { + "description": "A Pushover YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Pushover YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Pushover YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/id.json b/homeassistant/components/pushover/translations/id.json index 347f4ef5d3e..5643fd5aa3b 100644 --- a/homeassistant/components/pushover/translations/id.json +++ b/homeassistant/components/pushover/translations/id.json @@ -26,9 +26,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Proses konfigurasi Integrasi Pushover lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Integrasi Pushover dalam proses penghapusan" + "removed_yaml": { + "description": "Proses konfigurasi Integrasi Pushover lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Pushover telah dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/it.json b/homeassistant/components/pushover/translations/it.json index 8eec84415b7..3ce54d1c6d5 100644 --- a/homeassistant/components/pushover/translations/it.json +++ b/homeassistant/components/pushover/translations/it.json @@ -26,12 +26,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "La configurazione di Pushover tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Pushover dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Pushover sar\u00e0 rimossa" - }, "removed_yaml": { - "title": "La configurazione Pushover YAML \u00e8 stata rimossa" + "description": "La configurazione di Pushover tramite YAML \u00e8 stata rimossa.\n\nLa configurazione YAML esistente non sar\u00e0 utilizzata da Home Assistant.\n\nRimuovere la configurazione YAML di Pushover dal file configuration.yaml e riavviare Home Assistant per risolvere il problema.", + "title": "La configurazione YAML di Pushover \u00e8 stata rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/ja.json b/homeassistant/components/pushover/translations/ja.json index 9a229d5d08a..af5eb3eda52 100644 --- a/homeassistant/components/pushover/translations/ja.json +++ b/homeassistant/components/pushover/translations/ja.json @@ -24,11 +24,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Pushover\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Pushover\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "title": "Pushover YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/no.json b/homeassistant/components/pushover/translations/no.json index dd8713b73a4..141e0326c8c 100644 --- a/homeassistant/components/pushover/translations/no.json +++ b/homeassistant/components/pushover/translations/no.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Pushover med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Pushover YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "Pushover YAML-konfigurasjonen blir fjernet" - }, "removed_yaml": { "description": "Konfigurering av Pushover med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern Pushover YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", "title": "Pushover YAML-konfigurasjonen er fjernet" diff --git a/homeassistant/components/pushover/translations/pl.json b/homeassistant/components/pushover/translations/pl.json index 46b3596dd1c..1066a5c725a 100644 --- a/homeassistant/components/pushover/translations/pl.json +++ b/homeassistant/components/pushover/translations/pl.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Konfiguracja Pushover przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Konfiguracja YAML dla Pushover zostanie usuni\u0119ta" - }, "removed_yaml": { "description": "Konfiguracja Pushover za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", "title": "Konfiguracja YAML dla Pushover zosta\u0142a usuni\u0119ta" diff --git a/homeassistant/components/pushover/translations/pt-BR.json b/homeassistant/components/pushover/translations/pt-BR.json index 6e2f6412602..0b11e4ea2a6 100644 --- a/homeassistant/components/pushover/translations/pt-BR.json +++ b/homeassistant/components/pushover/translations/pt-BR.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o do Pushover usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o do Pushover YAML est\u00e1 sendo removida" - }, "removed_yaml": { "description": "A configura\u00e7\u00e3o do Pushover usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A configura\u00e7\u00e3o YAML do Pushover foi removida" diff --git a/homeassistant/components/pushover/translations/pt.json b/homeassistant/components/pushover/translations/pt.json index 519ac06a01f..9b06668589f 100644 --- a/homeassistant/components/pushover/translations/pt.json +++ b/homeassistant/components/pushover/translations/pt.json @@ -7,11 +7,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o do Pushover usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o do Pushover YAML est\u00e1 sendo removida" - } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/ru.json b/homeassistant/components/pushover/translations/ru.json index c6a1aff57b0..6a4095e67b4 100644 --- a/homeassistant/components/pushover/translations/ru.json +++ b/homeassistant/components/pushover/translations/ru.json @@ -26,9 +26,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushover \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushover \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Pushover\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushover \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/sk.json b/homeassistant/components/pushover/translations/sk.json new file mode 100644 index 00000000000..cf835f57ac7 --- /dev/null +++ b/homeassistant/components/pushover/translations/sk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "invalid_user_key": "Neplatn\u00fd k\u013e\u00fa\u010d pou\u017e\u00edvate\u013ea" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + }, + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov", + "user_key": "Pou\u017e\u00edvate\u013esk\u00fd k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/sv.json b/homeassistant/components/pushover/translations/sv.json index 5e85503bb4b..e6bfee469c6 100644 --- a/homeassistant/components/pushover/translations/sv.json +++ b/homeassistant/components/pushover/translations/sv.json @@ -24,11 +24,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Pushover med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Pushover YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Pushover YAML-konfigurationen tas bort" - } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/tr.json b/homeassistant/components/pushover/translations/tr.json index 5ab133b70a9..2e5ef735b42 100644 --- a/homeassistant/components/pushover/translations/tr.json +++ b/homeassistant/components/pushover/translations/tr.json @@ -26,10 +26,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "YAML kullanarak Pushover'\u0131 yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Pushover YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "Pushover YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" - }, "removed_yaml": { "description": "Pushover'i YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", "title": "Pushover YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" diff --git a/homeassistant/components/pushover/translations/zh-Hant.json b/homeassistant/components/pushover/translations/zh-Hant.json index 82f4f8f2d94..2051bc6ea94 100644 --- a/homeassistant/components/pushover/translations/zh-Hant.json +++ b/homeassistant/components/pushover/translations/zh-Hant.json @@ -26,12 +26,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Pushover \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushover YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Pushover YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" - }, "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Pushover \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushover YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Pushover \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushover YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Pushover YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/bg.json b/homeassistant/components/pvoutput/translations/bg.json index a580355fe12..8ec410d2f18 100644 --- a/homeassistant/components/pvoutput/translations/bg.json +++ b/homeassistant/components/pvoutput/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/pvoutput/translations/sk.json b/homeassistant/components/pvoutput/translations/sk.json index 4eba3bdc8bb..027281e1819 100644 --- a/homeassistant/components/pvoutput/translations/sk.json +++ b/homeassistant/components/pvoutput/translations/sk.json @@ -4,6 +4,7 @@ "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { @@ -14,7 +15,8 @@ }, "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" + "api_key": "API k\u013e\u00fa\u010d", + "system_id": "ID syst\u00e9mu" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sk.json b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json new file mode 100644 index 00000000000..009602df740 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov sn\u00edma\u010da", + "power": "Zmluvn\u00fd v\u00fdkon (kW)", + "tariff": "Platn\u00e1 tarifa pod\u013ea geografickej z\u00f3ny" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Zmluvn\u00fd v\u00fdkon (kW)", + "tariff": "Platn\u00e1 tarifa pod\u013ea geografickej z\u00f3ny" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index 046792a2ff2..b3cb80ad0f2 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -22,9 +22,10 @@ from homeassistant.components.bluetooth.passive_update_processor import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { QingpingBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( @@ -52,7 +53,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/qingping/device.py b/homeassistant/components/qingping/device.py index 4e4f29b8db8..ec6bb23c2af 100644 --- a/homeassistant/components/qingping/device.py +++ b/homeassistant/components/qingping/device.py @@ -1,13 +1,11 @@ """Support for Qingping devices.""" from __future__ import annotations -from qingping_ble import DeviceKey, SensorDeviceInfo +from qingping_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo def device_key_to_bluetooth_entity_key( @@ -15,17 +13,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a qingping device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py index 1affd320af2..6f1ad8118ab 100644 --- a/homeassistant/components/qingping/sensor.py +++ b/homeassistant/components/qingping/sensor.py @@ -34,9 +34,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { (QingpingSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( @@ -120,7 +121,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/qingping/translations/he.json b/homeassistant/components/qingping/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/qingping/translations/he.json +++ b/homeassistant/components/qingping/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/qingping/translations/sk.json b/homeassistant/components/qingping/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/qingping/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index 366bbdc3479..94e94dcb6ee 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -5,5 +5,6 @@ "requirements": ["georss_qld_bushfire_alert_client==0.5"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["georss_qld_bushfire_alert_client"] + "loggers": ["georss_qld_bushfire_alert_client"], + "integration_type": "service" } diff --git a/homeassistant/components/qnap_qsw/binary_sensor.py b/homeassistant/components/qnap_qsw/binary_sensor.py index 71af89778b8..aeebb6cc055 100644 --- a/homeassistant/components/qnap_qsw/binary_sensor.py +++ b/homeassistant/components/qnap_qsw/binary_sensor.py @@ -1,10 +1,18 @@ """Support for the QNAP QSW binary sensors.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Final -from aioqsw.const import QSD_ANOMALY, QSD_FIRMWARE_CONDITION, QSD_MESSAGE +from aioqsw.const import ( + QSD_ANOMALY, + QSD_FIRMWARE_CONDITION, + QSD_LACP_PORTS, + QSD_LINK, + QSD_MESSAGE, + QSD_PORTS, + QSD_PORTS_STATUS, +) from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -18,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_MESSAGE, DOMAIN, QSW_COORD_DATA from .coordinator import QswDataCoordinator -from .entity import QswEntityDescription, QswSensorEntity +from .entity import QswEntityDescription, QswEntityType, QswSensorEntity @dataclass @@ -28,6 +36,8 @@ class QswBinarySensorEntityDescription( """A class that describes QNAP QSW binary sensor entities.""" attributes: dict[str, list[str]] | None = None + qsw_type: QswEntityType | None = None + sep_key: str = "_" BINARY_SENSOR_TYPES: Final[tuple[QswBinarySensorEntityDescription, ...]] = ( @@ -43,20 +53,77 @@ BINARY_SENSOR_TYPES: Final[tuple[QswBinarySensorEntityDescription, ...]] = ( ), ) +LACP_PORT_BINARY_SENSOR_TYPES: Final[tuple[QswBinarySensorEntityDescription, ...]] = ( + QswBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_registry_enabled_default=False, + key=QSD_PORTS_STATUS, + qsw_type=QswEntityType.LACP_PORT, + name="Link", + subkey=QSD_LINK, + ), +) + +PORT_BINARY_SENSOR_TYPES: Final[tuple[QswBinarySensorEntityDescription, ...]] = ( + QswBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_registry_enabled_default=False, + key=QSD_PORTS_STATUS, + qsw_type=QswEntityType.PORT, + name="Link", + subkey=QSD_LINK, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW binary sensors from a config_entry.""" coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] - async_add_entities( - QswBinarySensor(coordinator, description, entry) - for description in BINARY_SENSOR_TYPES + + entities: list[QswBinarySensor] = [] + + for description in BINARY_SENSOR_TYPES: if ( description.key in coordinator.data and description.subkey in coordinator.data[description.key] - ) - ) + ): + entities.append(QswBinarySensor(coordinator, description, entry)) + + for description in LACP_PORT_BINARY_SENSOR_TYPES: + if ( + description.key in coordinator.data + and QSD_LACP_PORTS in coordinator.data[description.key] + ): + for port_id, port_values in coordinator.data[description.key][ + QSD_LACP_PORTS + ].items(): + if description.subkey in port_values: + _desc = replace( + description, + sep_key=f"_lacp_port_{port_id}_", + name=f"LACP Port {port_id} {description.name}", + ) + entities.append(QswBinarySensor(coordinator, _desc, entry, port_id)) + + for description in PORT_BINARY_SENSOR_TYPES: + if ( + description.key in coordinator.data + and QSD_PORTS in coordinator.data[description.key] + ): + for port_id, port_values in coordinator.data[description.key][ + QSD_PORTS + ].items(): + if description.subkey in port_values: + _desc = replace( + description, + sep_key=f"_port_{port_id}_", + name=f"Port {port_id} {description.name}", + ) + entities.append(QswBinarySensor(coordinator, _desc, entry, port_id)) + + async_add_entities(entities) class QswBinarySensor(QswSensorEntity, BinarySensorEntity): @@ -69,13 +136,13 @@ class QswBinarySensor(QswSensorEntity, BinarySensorEntity): coordinator: QswDataCoordinator, description: QswBinarySensorEntityDescription, entry: ConfigEntry, + type_id: int | None = None, ) -> None: """Initialize.""" - super().__init__(coordinator, entry) + super().__init__(coordinator, entry, type_id) + self._attr_name = f"{self.product} {description.name}" - self._attr_unique_id = ( - f"{entry.unique_id}_{description.key}_{description.subkey}" - ) + self._attr_unique_id = f"{entry.unique_id}_{description.key}{description.sep_key}{description.subkey}" self.entity_description = description self._async_update_attrs() @@ -83,6 +150,8 @@ class QswBinarySensor(QswSensorEntity, BinarySensorEntity): def _async_update_attrs(self) -> None: """Update binary sensor attributes.""" self._attr_is_on = self.get_device_value( - self.entity_description.key, self.entity_description.subkey + self.entity_description.key, + self.entity_description.subkey, + self.entity_description.qsw_type, ) super()._async_update_attrs() diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index 794e8c67baa..e0d4a1f78cd 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -78,6 +78,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("DHCP discovery detected QSW: %s", self._discovered_mac) + await self.async_set_unique_id(format_mac(self._discovered_mac)) + self._abort_if_unique_id_configured( + updates={ + CONF_URL: self._discovered_url, + } + ) + options = ConnectionOptions(self._discovered_url, "", "") qsw = QnapQswApi(aiohttp_client.async_get_clientsession(self.hass), options) @@ -86,9 +93,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except QswError as err: raise AbortFlow("cannot_connect") from err - await self.async_set_unique_id(format_mac(self._discovered_mac)) - self._abort_if_unique_id_configured() - return await self.async_step_discovered_connection() async def async_step_discovered_connection( diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index 7da47f9734f..7d1ec33ee71 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -7,11 +7,14 @@ from typing import Any from aioqsw.const import ( QSD_FIRMWARE, QSD_FIRMWARE_INFO, + QSD_LACP_PORTS, QSD_MAC, + QSD_PORTS, QSD_PRODUCT, QSD_SYSTEM_BOARD, ) +from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import callback @@ -23,6 +26,13 @@ from .const import MANUFACTURER from .coordinator import QswDataCoordinator, QswFirmwareCoordinator +class QswEntityType(StrEnum): + """QNAP QSW Entity Type.""" + + LACP_PORT = QSD_LACP_PORTS + PORT = QSD_PORTS + + class QswDataEntity(CoordinatorEntity[QswDataCoordinator]): """Define an QNAP QSW entity.""" @@ -30,10 +40,12 @@ class QswDataEntity(CoordinatorEntity[QswDataCoordinator]): self, coordinator: QswDataCoordinator, entry: ConfigEntry, + type_id: int | None = None, ) -> None: """Initialize.""" super().__init__(coordinator) + self.type_id = type_id self.product = self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT) self._attr_device_info = DeviceInfo( configuration_url=entry.data[CONF_URL], @@ -49,12 +61,24 @@ class QswDataEntity(CoordinatorEntity[QswDataCoordinator]): sw_version=self.get_device_value(QSD_FIRMWARE_INFO, QSD_FIRMWARE), ) - def get_device_value(self, key: str, subkey: str) -> Any: + def get_device_value( + self, + key: str, + subkey: str, + qsw_type: QswEntityType | None = None, + ) -> Any: """Return device value by key.""" value = None if key in self.coordinator.data: data = self.coordinator.data[key] - if subkey in data: + if qsw_type is not None and self.type_id is not None: + if ( + qsw_type in data + and self.type_id in data[qsw_type] + and subkey in data[qsw_type][self.type_id] + ): + value = data[qsw_type][self.type_id][subkey] + elif subkey in data: value = data[subkey] return value diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index 618c20b4cc2..5fecf28c9f2 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -7,10 +7,20 @@ from typing import Final from aioqsw.const import ( QSD_FAN1_SPEED, QSD_FAN2_SPEED, + QSD_LINK, + QSD_PORT_NUM, + QSD_PORTS_STATISTICS, + QSD_PORTS_STATUS, + QSD_RX_ERRORS, + QSD_RX_OCTETS, + QSD_RX_SPEED, + QSD_SYSTEM_BOARD, QSD_SYSTEM_SENSOR, QSD_SYSTEM_TIME, QSD_TEMP, QSD_TEMP_MAX, + QSD_TX_OCTETS, + QSD_TX_SPEED, QSD_UPTIME, ) @@ -21,7 +31,12 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TIME_SECONDS +from homeassistant.const import ( + DATA_BYTES, + DATA_RATE_BYTES_PER_SECOND, + TEMP_CELSIUS, + TIME_SECONDS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -55,6 +70,44 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( state_class=SensorStateClass.MEASUREMENT, subkey=QSD_FAN2_SPEED, ), + QswSensorEntityDescription( + attributes={ + ATTR_MAX: [QSD_SYSTEM_BOARD, QSD_PORT_NUM], + }, + entity_registry_enabled_default=False, + icon="mdi:ethernet", + key=QSD_PORTS_STATUS, + name="Ports", + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_LINK, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX", + native_unit_of_measurement=DATA_BYTES, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_OCTETS, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:close-network", + key=QSD_PORTS_STATISTICS, + entity_category=EntityCategory.DIAGNOSTIC, + name="RX Errors", + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_ERRORS, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX Speed", + native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_RX_SPEED, + ), QswSensorEntityDescription( attributes={ ATTR_MAX: [QSD_SYSTEM_SENSOR, QSD_TEMP_MAX], @@ -66,6 +119,24 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( state_class=SensorStateClass.MEASUREMENT, subkey=QSD_TEMP, ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX", + native_unit_of_measurement=DATA_BYTES, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_TX_OCTETS, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX Speed", + native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_TX_SPEED, + ), QswSensorEntityDescription( icon="mdi:timer-outline", key=QSD_SYSTEM_TIME, @@ -116,7 +187,8 @@ class QswSensor(QswSensorEntity, SensorEntity): @callback def _async_update_attrs(self) -> None: """Update sensor attributes.""" - self._attr_native_value = self.get_device_value( + value = self.get_device_value( self.entity_description.key, self.entity_description.subkey ) + self._attr_native_value = value super()._async_update_attrs() diff --git a/homeassistant/components/qnap_qsw/translations/sk.json b/homeassistant/components/qnap_qsw/translations/sk.json new file mode 100644 index 00000000000..d1536578450 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/sk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "invalid_id": "Zariadenie vr\u00e1tilo neplatn\u00e9 jedine\u010dn\u00e9 ID" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 666cc0c93ce..1a394e17f29 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.2.0", "pyzbar==0.1.7"], + "requirements": ["pillow==9.3.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated", "loggers": ["pyzbar"] diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 67e36dab203..6ab918f26fe 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -71,8 +71,6 @@ class QVRProCamera(Camera): self._client = client self._stream_source = stream_source - self._supported_features = 0 - super().__init__() @property @@ -113,8 +111,3 @@ class QVRProCamera(Camera): async def stream_source(self): """Get stream source.""" return self._stream_source - - @property - def supported_features(self): - """Get supported features.""" - return self._supported_features diff --git a/homeassistant/components/rachio/translations/bg.json b/homeassistant/components/rachio/translations/bg.json index fdbdc5b1cdf..969022c860a 100644 --- a/homeassistant/components/rachio/translations/bg.json +++ b/homeassistant/components/rachio/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rachio/translations/sk.json b/homeassistant/components/rachio/translations/sk.json index ff853127803..89ef7514ac8 100644 --- a/homeassistant/components/rachio/translations/sk.json +++ b/homeassistant/components/rachio/translations/sk.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { @@ -10,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Trvanie v min\u00fatach pri aktiv\u00e1cii sp\u00edna\u010da z\u00f3ny" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index 403bedda94a..3584d5242b6 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -6,12 +6,10 @@ from typing import Any, cast from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_SW_VERSION, CONF_API_KEY, - CONF_PLATFORM, CONF_URL, CONF_VERIFY_SSL, Platform, @@ -20,8 +18,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN @@ -37,26 +33,6 @@ from .coordinator import ( PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Steam integration.""" - if SENSOR_DOMAIN not in config: - return True - - for entry in config[SENSOR_DOMAIN]: - if entry[CONF_PLATFORM] == DOMAIN: - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Radarr from a config entry.""" host_configuration = PyArrHostConfiguration( diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py index c37eeba4969..3feb9a01bea 100644 --- a/homeassistant/components/radarr/config_flow.py +++ b/homeassistant/components/radarr/config_flow.py @@ -11,19 +11,12 @@ from aiopyarr.radarr_client import RadarrClient import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_URL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN, LOGGER +from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): @@ -106,28 +99,6 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - for entry in self._async_current_entries(): - if entry.data[CONF_API_KEY] == config[CONF_API_KEY]: - _part = config[CONF_API_KEY][0:4] - _msg = f"Radarr yaml config with partial key {_part} has been imported. Please remove it" - LOGGER.warning(_msg) - return self.async_abort(reason="already_configured") - proto = "https" if config[CONF_SSL] else "http" - host_port = f"{config[CONF_HOST]}:{config[CONF_PORT]}" - path = "" - if config["urlbase"].rstrip("/") not in ("", "/", "/api"): - path = config["urlbase"].rstrip("/") - return self.async_create_entry( - title=DEFAULT_NAME, - data={ - CONF_URL: f"{proto}://{host_port}{path}", - CONF_API_KEY: config[CONF_API_KEY], - CONF_VERIFY_SSL: False, - }, - ) - async def validate_input( hass: HomeAssistant, data: dict[str, Any] diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 5117fd161d3..1d1ce5b0289 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["aiopyarr"] + "loggers": ["aiopyarr"], + "integration_type": "service" } diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 27d1a5487a2..0f244b92a16 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -8,30 +8,23 @@ from datetime import datetime, timezone from typing import Any, Generic from aiopyarr import Diskspace, RootFolder, SystemStatus -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_MONITORED_CONDITIONS, - CONF_PORT, - CONF_SSL, DATA_BYTES, DATA_GIGABYTES, DATA_KILOBYTES, DATA_MEGABYTES, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import RadarrEntity @@ -112,22 +105,6 @@ BYTE_SIZES = [ DATA_MEGABYTES, DATA_GIGABYTES, ] -# Deprecated in Home Assistant 2022.10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional("days", default=1): cv.string, - vol.Optional(CONF_HOST, default="localhost"): cv.string, - vol.Optional("include_paths", default=[]): cv.ensure_list, - vol.Optional(CONF_MONITORED_CONDITIONS, default=["movies"]): vol.All( - cv.ensure_list - ), - vol.Optional(CONF_PORT, default=7878): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional("unit", default=DATA_GIGABYTES): cv.string, - vol.Optional("urlbase", default=""): cv.string, - } -) PARALLEL_UPDATES = 1 @@ -139,10 +116,14 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Radarr platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", ) diff --git a/homeassistant/components/radarr/strings.json b/homeassistant/components/radarr/strings.json index 6fa9b64c2c8..299dd0a56b0 100644 --- a/homeassistant/components/radarr/strings.json +++ b/homeassistant/components/radarr/strings.json @@ -36,9 +36,9 @@ } }, "issues": { - "deprecated_yaml": { - "title": "The Radarr YAML configuration is being removed", - "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "removed_yaml": { + "title": "The Radarr YAML configuration has been removed", + "description": "Configuring Radarr using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/radarr/translations/bg.json b/homeassistant/components/radarr/translations/bg.json index 562883a2f23..070befea137 100644 --- a/homeassistant/components/radarr/translations/bg.json +++ b/homeassistant/components/radarr/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -27,8 +27,9 @@ "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Radarr \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Radarr \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Radarr \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" }, - "removed_attributes": { - "title": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 \u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Radarr" + "removed_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Radarr \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Radarr \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" } }, "options": { diff --git a/homeassistant/components/radarr/translations/ca.json b/homeassistant/components/radarr/translations/ca.json index c94cd1cd901..de8c3861de9 100644 --- a/homeassistant/components/radarr/translations/ca.json +++ b/homeassistant/components/radarr/translations/ca.json @@ -31,9 +31,9 @@ "description": "La configuraci\u00f3 de Radarr mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Radarr del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Radarr est\u00e0 sent eliminada" }, - "removed_attributes": { - "description": "Per precauci\u00f3, s'han fet alguns canvis importants en el sensor recompte de pel\u00b7l\u00edcules.\n\nAquest sensor pot causar problemes amb bases de dades molt grans. Si encara vols utilitzar-lo, pots fer-ho. \n\nEls noms de pel\u00b7l\u00edcules ja no s'inclouen com a atributs al sensor pel\u00b7l\u00edcules. \n\nPropers s'ha eliminat. S'ha modernitzat com a elements de calendari. L'espai del disc ara es divideix en diferents sensors, un per a cada carpeta. \n\nL'estat i les comandes s'han eliminat perqu\u00e8 no sembla que tinguin un valor real per a les automatitzacions.", - "title": "Canvis a la integraci\u00f3 Radarr" + "removed_yaml": { + "description": "La configuraci\u00f3 de Radarr mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Radarr s'ha eliminat" } }, "options": { diff --git a/homeassistant/components/radarr/translations/cs.json b/homeassistant/components/radarr/translations/cs.json new file mode 100644 index 00000000000..08b98b728ff --- /dev/null +++ b/homeassistant/components/radarr/translations/cs.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "zeroconf_failed": "Kl\u00ed\u010d API nenalezen. Zadejte jej pros\u00edm ru\u010dn\u011b" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "url": "URL", + "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "Konfigurace Radarr YAML se odstra\u0148uje" + }, + "removed_yaml": { + "description": "Konfigurace Radaru pomoc\u00ed YAML byla odstran\u011bna. \n\nVa\u0161i st\u00e1vaj\u00edc\u00ed konfigurace YAML nen\u00ed vyu\u017eita Home Assistantem. \n\nOdstra\u0148te konfiguraci YAML ze souboru configuration.yaml a restartujte Home Assistant, abyste se tento probl\u00e9m vy\u0159e\u0161il.", + "title": "Konfigurace Radarr YAML byla odstran\u011bna" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Po\u010det nadch\u00e1zej\u00edc\u00edch dn\u016f k zobrazen\u00ed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/de.json b/homeassistant/components/radarr/translations/de.json index 81aafd0a351..9e6a805ad94 100644 --- a/homeassistant/components/radarr/translations/de.json +++ b/homeassistant/components/radarr/translations/de.json @@ -13,7 +13,7 @@ }, "step": { "reauth_confirm": { - "description": "Die Radarr-Integration muss manuell erneut mit der Radarr-API authentifiziert werden", + "description": "Die Radarr Integration muss manuell erneut mit der Radarr-API authentifiziert werden", "title": "Integration erneut authentifizieren" }, "user": { @@ -22,18 +22,18 @@ "url": "URL", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, - "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest unter Einstellungen > Allgemein in der Radarr-Web-Benutzeroberfl\u00e4che." + "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest du unter Einstellungen \u2192 Allgemein in der Radarr-Web-Benutzeroberfl\u00e4che." } } }, "issues": { "deprecated_yaml": { "description": "Die Konfiguration von Radarr mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Radarr-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Radarr-YAML-Konfiguration wird entfernt" + "title": "Die Radarr YAML-Konfiguration wird entfernt" }, - "removed_attributes": { - "description": "Es wurden einige \u00c4nderungen vorgenommen, um den Filmz\u00e4hlsensor aus Vorsicht zu deaktivieren.\n\nDieser Sensor kann bei gro\u00dfen Datenbanken Probleme verursachen. Wenn du ihn dennoch verwenden m\u00f6chtest, kannst du dies tun.\n\nFilmnamen werden nicht mehr als Attribute in den Filmsensor aufgenommen.\n\nUpcoming wurde entfernt. Er wird modernisiert, so wie es bei Kalenderelementen sein sollte. Der Speicherplatz ist jetzt in verschiedene Sensoren aufgeteilt, einen f\u00fcr jeden Ordner.\n\nStatus und Befehle wurden entfernt, da sie f\u00fcr Automatisierungen keinen wirklichen Wert zu haben scheinen.", - "title": "\u00c4nderungen an der Radarr-Integration" + "removed_yaml": { + "description": "Die Konfiguration von Radarr mittels YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird vom Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Radarr YAML-Konfiguration wurde entfernt" } }, "options": { diff --git a/homeassistant/components/radarr/translations/el.json b/homeassistant/components/radarr/translations/el.json index f7eb031cba5..e10f213a156 100644 --- a/homeassistant/components/radarr/translations/el.json +++ b/homeassistant/components/radarr/translations/el.json @@ -31,9 +31,9 @@ "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Radarr YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Radarr YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" }, - "removed_attributes": { - "description": "\u0388\u03b3\u03b9\u03bd\u03b1\u03bd \u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd \u03c7\u03c9\u03c1\u03af\u03c2 \u03c0\u03c1\u03bf\u03c3\u03bf\u03c7\u03ae. \n\n \u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03c1\u03bf\u03ba\u03b1\u03bb\u03ad\u03c3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b5\u03c1\u03ac\u03c3\u03c4\u03b9\u03b5\u03c2 \u03b2\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd. \u0395\u03ac\u03bd \u03b5\u03be\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5. \n\n \u03a4\u03b1 \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c9\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd. \n\n \u03a4\u03bf \u03b5\u03c0\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af. \u0395\u03ba\u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03cc\u03c0\u03c9\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03b7\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5. \u039f \u03c7\u03ce\u03c1\u03bf\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03c3\u03ba\u03bf \u03c7\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c3\u03b5 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2, \u03ad\u03bd\u03b1\u03bd \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf. \n\n \u0397 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae \u03b1\u03be\u03af\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2.", - "title": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr" + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Radarr YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" } }, "options": { diff --git a/homeassistant/components/radarr/translations/en.json b/homeassistant/components/radarr/translations/en.json index 168c3cc2fe2..928dc3a99f9 100644 --- a/homeassistant/components/radarr/translations/en.json +++ b/homeassistant/components/radarr/translations/en.json @@ -31,9 +31,9 @@ "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Radarr YAML configuration is being removed" }, - "removed_attributes": { - "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations.", - "title": "Changes to the Radarr integration" + "removed_yaml": { + "description": "Configuring Radarr using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Radarr YAML configuration has been removed" } }, "options": { diff --git a/homeassistant/components/radarr/translations/es.json b/homeassistant/components/radarr/translations/es.json index 5f88c7ef8f4..c3d087fa3b0 100644 --- a/homeassistant/components/radarr/translations/es.json +++ b/homeassistant/components/radarr/translations/es.json @@ -31,9 +31,9 @@ "description": "Se va a eliminar la configuraci\u00f3n de Radarr mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Radarr de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Radarr" }, - "removed_attributes": { - "description": "Se han realizado algunos cambios importantes al deshabilitar el sensor de conteo de pel\u00edculas por precauci\u00f3n. \n\nEste sensor puede causar problemas con bases de datos enormes. Si a\u00fan deseas utilizarlo, puedes hacerlo. \n\nLos nombres de las pel\u00edculas ya no se incluyen como atributos en el sensor de pel\u00edculas. \n\nPr\u00f3ximamente ha sido eliminado. Se est\u00e1 modernizando como deber\u00edan ser los elementos del calendario. El espacio en disco ahora se divide en diferentes sensores, uno para cada carpeta. \n\nEl estado y los comandos se han eliminado porque no parecen tener un valor real para las automatizaciones.", - "title": "Cambios en la integraci\u00f3n de Radarr" + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Radarr usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Radarr" } }, "options": { diff --git a/homeassistant/components/radarr/translations/et.json b/homeassistant/components/radarr/translations/et.json index 0f91bc2f47b..64c48f67220 100644 --- a/homeassistant/components/radarr/translations/et.json +++ b/homeassistant/components/radarr/translations/et.json @@ -31,9 +31,9 @@ "description": "Radarri seadistamine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage failist configuration.yaml radarr YAML konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", "title": "Radarr YAML-i konfiguratsiooni eemaldatakse" }, - "removed_attributes": { - "description": "M\u00f5ned murrangulised muudatused on tehtud liikumiste arvuanduri v\u00e4ljal\u00fclitamisel ettevaatusabin\u00f5ude t\u00f5ttu.\n\nSee andur v\u00f5ib p\u00f5hjustada probleeme massiivsete andmebaaside puhul. Kui soovite seda siiski kasutada, v\u00f5ite seda teha.\n\nFilmide nimed ei ole enam filmide anduri atribuutidena lisatud.\n\nTulevikus on eemaldatud. Seda ajakohastatakse, nagu kalendrielemendid peaksid olema. Kettaruum on n\u00fc\u00fcd jagatud erinevateks anduriteks, \u00fcks iga kausta jaoks.\n\nStaatus ja k\u00e4sud on eemaldatud, kuna neil ei tundu olevat t\u00f5elist v\u00e4\u00e4rtust automaatika jaoks.", - "title": "Muudatused Radarri integratsioonis" + "removed_yaml": { + "description": "Radarri konfigureerimine YAML-i abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Radarr YAML-i konfiguratsioon on eemaldatud" } }, "options": { diff --git a/homeassistant/components/radarr/translations/fr.json b/homeassistant/components/radarr/translations/fr.json index 5f8d2a9f78d..9dbf28c5129 100644 --- a/homeassistant/components/radarr/translations/fr.json +++ b/homeassistant/components/radarr/translations/fr.json @@ -28,9 +28,6 @@ "issues": { "deprecated_yaml": { "title": "La configuration YAML pour Radarr sera bient\u00f4t supprim\u00e9e" - }, - "removed_attributes": { - "title": "Modifications apport\u00e9es \u00e0 l'int\u00e9gration Radarr" } }, "options": { diff --git a/homeassistant/components/radarr/translations/hu.json b/homeassistant/components/radarr/translations/hu.json index f00034f5f5f..cf5a08d3fec 100644 --- a/homeassistant/components/radarr/translations/hu.json +++ b/homeassistant/components/radarr/translations/hu.json @@ -30,10 +30,6 @@ "deprecated_yaml": { "description": "A Radarr YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Radarr YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "A Radarr YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - }, - "removed_attributes": { - "description": "N\u00e9h\u00e1ny v\u00e1ltoztat\u00e1s t\u00f6rt\u00e9nt a Filmek sz\u00e1m\u00e1nak \u00e9rz\u00e9kel\u0151j\u00e9nek \u00f3vatoss\u00e1gb\u00f3l t\u00f6rt\u00e9n\u0151 letilt\u00e1s\u00e1ban.\n\nEz az \u00e9rz\u00e9kel\u0151 probl\u00e9m\u00e1kat okozhat hatalmas adatb\u00e1zisok eset\u00e9n. Ha tov\u00e1bbra is haszn\u00e1lni szeretn\u00e9, megteheti.\n\nA filmek nevei t\u00f6bb\u00e9 nem szerepelnek attrib\u00fatumk\u00e9nt a filmek \u00e9rz\u00e9kel\u0151ben.\n\nAz Upcoming elt\u00e1vol\u00edt\u00e1sra ker\u00fclt. Korszer\u0171s\u00edt\u00e9sre ker\u00fcl, ahogyan a napt\u00e1relemeknek is kell. A lemezter\u00fclet mostant\u00f3l k\u00fcl\u00f6nb\u00f6z\u0151 \u00e9rz\u00e9kel\u0151kre van felosztva, egy-egy mapp\u00e1hoz.\n\nA st\u00e1tusz \u00e9s a parancsok elt\u00e1vol\u00edt\u00e1sra ker\u00fcltek, mivel \u00fagy t\u0171nik, hogy nincs val\u00f3di \u00e9rt\u00e9k\u00fck az automatiz\u00e1l\u00e1sok sz\u00e1m\u00e1ra.", - "title": "A Radarr-integr\u00e1ci\u00f3 v\u00e1ltoz\u00e1sai" } }, "options": { diff --git a/homeassistant/components/radarr/translations/id.json b/homeassistant/components/radarr/translations/id.json index 95f19f7136e..97311c8f48b 100644 --- a/homeassistant/components/radarr/translations/id.json +++ b/homeassistant/components/radarr/translations/id.json @@ -31,9 +31,9 @@ "description": "Proses konfigurasi Integrasi Radarr lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Radarr dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", "title": "Konfigurasi YAML Integrasi Radarr dalam proses penghapusan" }, - "removed_attributes": { - "description": "Beberapa perubahan besar telah dilakukan dalam menonaktifkan sensor hitungan Film dengan alasan kehati-hatian.\n\nSensor ini bisa menyebabkan masalah dengan database yang sangat besar. Jika masih ingin menggunakannya, Anda dapat melakukannya.\n\nNama film tidak lagi disertakan sebagai atribut dalam sensor film.\n\nItem \"Yang akan datang\" telah dihapus. Sensor ini sedang dimodernisasi sebagaimana layaknya item kalender. Ruang disk sekarang dipecah ke dalam sensor yang berbeda, satu untuk setiap folder.\n\nStatus dan perintah telah dihapus karena tampaknya tidak membawa nilai dalam otomasi.", - "title": "Perubahan pada integrasi Radarr" + "removed_yaml": { + "description": "Proses konfigurasi Integrasi Radarr lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Radarr telah dihapus" } }, "options": { diff --git a/homeassistant/components/radarr/translations/it.json b/homeassistant/components/radarr/translations/it.json index afc64b6bc8e..d6a1445493d 100644 --- a/homeassistant/components/radarr/translations/it.json +++ b/homeassistant/components/radarr/translations/it.json @@ -30,10 +30,6 @@ "deprecated_yaml": { "description": "La configurazione di Radarr tramite YAML \u00e8 stata rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Radarr dal file configuration.yaml e riavviare Home Assistant per risolvere il problema.", "title": "La configurazione YAML di Radarr \u00e8 stata rimossa" - }, - "removed_attributes": { - "description": "Sono state apportate alcune modifiche alla disabilitazione del sensore di conteggio dei filmati per prudenza.\n\nQuesto sensore pu\u00f2 causare problemi con database di grandi dimensioni. Se si desidera ancora utilizzarlo, \u00e8 possibile farlo.\n\nI nomi dei film non sono pi\u00f9 inclusi come attributi nel sensore dei film.\n\nLa voce Upcoming \u00e8 stata rimossa. \u00c8 stato modernizzato come dovrebbero essere gli elementi del calendario. Lo spazio su disco \u00e8 ora suddiviso in diversi sensori, uno per ogni cartella.\n\nStato e comandi sono stati rimossi perch\u00e9 non sembrano avere un valore reale per le automazioni.", - "title": "Modifiche all'integrazione Radarr" } }, "options": { diff --git a/homeassistant/components/radarr/translations/nl.json b/homeassistant/components/radarr/translations/nl.json index b4f1fcd7106..0047e41fe62 100644 --- a/homeassistant/components/radarr/translations/nl.json +++ b/homeassistant/components/radarr/translations/nl.json @@ -27,8 +27,8 @@ "deprecated_yaml": { "title": "De Radarr YAML-configuratie wordt verwijderd" }, - "removed_attributes": { - "title": "Wijzigingen in de Radarr-integratie" + "removed_yaml": { + "title": "De Radarr YAML configuratie is verwijderd" } } } \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/no.json b/homeassistant/components/radarr/translations/no.json index 4a86867a3fd..e74efdaf846 100644 --- a/homeassistant/components/radarr/translations/no.json +++ b/homeassistant/components/radarr/translations/no.json @@ -31,9 +31,9 @@ "description": "Konfigurering av Radarr med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Radarr YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", "title": "Radarr YAML-konfigurasjonen blir fjernet" }, - "removed_attributes": { - "description": "Noen bruddendringer er gjort for \u00e5 deaktivere filmtellingssensoren ut av forsiktighet. \n\n Denne sensoren kan for\u00e5rsake problemer med massive databaser. Hvis du fortsatt \u00f8nsker \u00e5 bruke den, kan du gj\u00f8re det. \n\n Filmnavn er ikke lenger inkludert som attributter i filmsensoren. \n\n Kommende er fjernet. Den moderniseres slik kalenderposter skal v\u00e6re. Diskplass er n\u00e5 delt inn i forskjellige sensorer, en for hver mappe. \n\n Status og kommandoer er fjernet da de ikke ser ut til \u00e5 ha reell verdi for automatisering.", - "title": "Endringer i Radarr-integrasjonen" + "removed_yaml": { + "description": "Konfigurering av Radarr med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Radarr YAML-konfigurasjonen er fjernet" } }, "options": { diff --git a/homeassistant/components/radarr/translations/pl.json b/homeassistant/components/radarr/translations/pl.json index ff7092eaff0..49d773258b9 100644 --- a/homeassistant/components/radarr/translations/pl.json +++ b/homeassistant/components/radarr/translations/pl.json @@ -30,10 +30,6 @@ "deprecated_yaml": { "description": "Konfiguracja Radarr przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", "title": "Konfiguracja YAML dla Radarr zostanie usuni\u0119ta" - }, - "removed_attributes": { - "description": "Z powodu ostro\u017cno\u015bci wprowadzono pewne prze\u0142omowe zmiany w wy\u0142\u0105czaniu sensora liczby film\u00f3w. \n\nTen sensor mo\u017ce powodowa\u0107 problemy z ogromnymi bazami danych. Je\u015bli nadal chcesz z niego korzysta\u0107, mo\u017cesz to zrobi\u0107. \n\n\"Nazwy film\u00f3w\" nie s\u0105 ju\u017c uwzgl\u0119dniane w atrybutach sensora film\u00f3w. \n\n\"Nadchodz\u0105ce\" zosta\u0142o usuni\u0119te. Jest unowocze\u015bniany tak, jak przysta\u0142o na kalendarz. \"Miejsce na dysku\" jest teraz podzielone na r\u00f3\u017cne sensory, po jednym dla ka\u017cdego folderu. \n\n\"Status\" i \"Polecenia\" zosta\u0142y usuni\u0119te, poniewa\u017c nie wydaj\u0105 si\u0119 mie\u0107 rzeczywistej warto\u015bci dla automatyzacji.", - "title": "Zmiany w integracji Radarr" } }, "options": { diff --git a/homeassistant/components/radarr/translations/pt-BR.json b/homeassistant/components/radarr/translations/pt-BR.json index 78b9078cb9f..a9c955561f9 100644 --- a/homeassistant/components/radarr/translations/pt-BR.json +++ b/homeassistant/components/radarr/translations/pt-BR.json @@ -31,9 +31,9 @@ "description": "A configura\u00e7\u00e3o do Radarr usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Radarr do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A configura\u00e7\u00e3o YAML do Radarr est\u00e1 sendo removida" }, - "removed_attributes": { - "description": "Algumas mudan\u00e7as importantes foram feitas na desativa\u00e7\u00e3o do sensor de contagem de filmes por precau\u00e7\u00e3o. \n\n Este sensor pode causar problemas com bancos de dados massivos. Se voc\u00ea ainda deseja us\u00e1-lo, voc\u00ea pode faz\u00ea-lo. \n\n Os nomes dos filmes n\u00e3o s\u00e3o mais inclu\u00eddos como atributos no sensor de filmes. \n\n O pr\u00f3ximo foi removido. Ele est\u00e1 sendo modernizado como os itens do calend\u00e1rio devem ser. O espa\u00e7o em disco agora \u00e9 dividido em diferentes sensores, um para cada pasta. \n\n Status e comandos foram removidos, pois n\u00e3o parecem ter valor real para automa\u00e7\u00f5es.", - "title": "Mudan\u00e7as na integra\u00e7\u00e3o do Radarr" + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Radarr usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de Radarr foi removida" } }, "options": { diff --git a/homeassistant/components/radarr/translations/ru.json b/homeassistant/components/radarr/translations/ru.json index 46fd877aa3a..7013a6e3a04 100644 --- a/homeassistant/components/radarr/translations/ru.json +++ b/homeassistant/components/radarr/translations/ru.json @@ -31,9 +31,9 @@ "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radarr \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radarr \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, - "removed_attributes": { - "description": "\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043c\u0435\u0440\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u043e\u0440\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0431\u044b\u043b \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0441\u0435\u043d\u0441\u043e\u0440 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u0444\u0438\u043b\u044c\u043c\u043e\u0432. \u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u043c\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u043c\u0438 \u0431\u0430\u0437\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445, \u043d\u043e \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u0412\u044b \u0432\u0441\u0435 \u0435\u0449\u0451 \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e.\n\n\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0444\u0438\u043b\u044c\u043c\u043e\u0432 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043e\u0432 \u0432 \u0441\u0435\u043d\u0441\u043e\u0440 \u0444\u0438\u043b\u044c\u043c\u043e\u0432.\n\n\u041f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0431\u044b\u043b\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043e. \u041e\u043d\u043e \u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043e, \u043a\u0430\u043a \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044f. \u0414\u0438\u0441\u043a\u043e\u0432\u043e\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e \u0442\u0435\u043f\u0435\u0440\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043e \u043d\u0430 \u0440\u0430\u0437\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b, \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u043f\u0430\u043f\u043a\u0438.\n\n\u0421\u0442\u0430\u0442\u0443\u0441 \u0438 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0431\u044b\u043b\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u044b, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d\u0438 \u043d\u0435 \u0438\u043c\u0435\u044e\u0442 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0446\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", - "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Radarr" + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Radarr\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radarr \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, "options": { diff --git a/homeassistant/components/radarr/translations/sk.json b/homeassistant/components/radarr/translations/sk.json new file mode 100644 index 00000000000..76e74a69f4a --- /dev/null +++ b/homeassistant/components/radarr/translations/sk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "zeroconf_failed": "K\u013e\u00fa\u010d API sa nena\u0161iel. Zadajte ho ru\u010dne" + }, + "step": { + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "url": "URL", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/sv.json b/homeassistant/components/radarr/translations/sv.json index a5f84833bd9..132fc4c4335 100644 --- a/homeassistant/components/radarr/translations/sv.json +++ b/homeassistant/components/radarr/translations/sv.json @@ -30,10 +30,6 @@ "deprecated_yaml": { "description": "Konfigurering av Radarr med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Radarr YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", "title": "Radarr YAML-konfigurationen tas bort" - }, - "removed_attributes": { - "description": "Vissa f\u00f6r\u00e4ndringar har gjorts f\u00f6r att inaktivera r\u00e4knesensorn f\u00f6r filmer av f\u00f6rsiktighet. \n\n Denna sensor kan orsaka problem med massiva databaser. Om du fortfarande vill anv\u00e4nda den kan du g\u00f6ra det. \n\n Filmnamn ing\u00e5r inte l\u00e4ngre som attribut i filmsensorn. \n\n Kommande har tagits bort. Det h\u00e5ller p\u00e5 att moderniseras som kalenderobjekt ska vara. Diskutrymme \u00e4r nu uppdelat i olika sensorer, en f\u00f6r varje mapp. \n\n Status och kommandon har tagits bort eftersom de inte verkar ha n\u00e5got verkligt v\u00e4rde f\u00f6r automatiseringar.", - "title": "\u00c4ndringar av Radarr-integrationen" } }, "options": { diff --git a/homeassistant/components/radarr/translations/tr.json b/homeassistant/components/radarr/translations/tr.json index 256cba85647..ef2eb8c5575 100644 --- a/homeassistant/components/radarr/translations/tr.json +++ b/homeassistant/components/radarr/translations/tr.json @@ -30,10 +30,6 @@ "deprecated_yaml": { "description": "Radarr'\u0131n YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Radarr YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", "title": "Radarr YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" - }, - "removed_attributes": { - "description": "Film sayma sens\u00f6r\u00fcn\u00fcn dikkatli bir \u015fekilde devre d\u0131\u015f\u0131 b\u0131rak\u0131lmas\u0131nda baz\u0131 \u00f6nemli de\u011fi\u015fiklikler yap\u0131ld\u0131. \n\n Bu sens\u00f6r, b\u00fcy\u00fck veritabanlar\u0131nda sorunlara neden olabilir. Hala kullanmak istiyorsan\u0131z, bunu yapabilirsiniz. \n\n Film adlar\u0131 art\u0131k film sens\u00f6r\u00fcne \u00f6znitelik olarak dahil edilmemektedir. \n\n Yakla\u015fan kald\u0131r\u0131ld\u0131. Takvim \u00f6\u011feleri olmas\u0131 gerekti\u011fi gibi modernize ediliyor. Disk alan\u0131 art\u0131k her klas\u00f6r i\u00e7in bir tane olmak \u00fczere farkl\u0131 sens\u00f6rlere b\u00f6l\u00fcnm\u00fc\u015ft\u00fcr. \n\n Otomasyonlar i\u00e7in ger\u00e7ek de\u011feri olmad\u0131\u011f\u0131 i\u00e7in durum ve komutlar kald\u0131r\u0131ld\u0131.", - "title": "Radarr entegrasyonundaki de\u011fi\u015fiklikler" } }, "options": { diff --git a/homeassistant/components/radarr/translations/zh-Hans.json b/homeassistant/components/radarr/translations/zh-Hans.json index fe3b28ff5b6..120079d5b62 100644 --- a/homeassistant/components/radarr/translations/zh-Hans.json +++ b/homeassistant/components/radarr/translations/zh-Hans.json @@ -20,5 +20,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Radarr\u7684YAML\u914d\u7f6e\u5df2\u88ab\u5220\u9664\n\nHome Assistant\u4e0d\u4f7f\u7528\u73b0\u6709\u7684 YAML \u914d\u7f6e\u3002\n\n\u4ece configuration.yaml \u6587\u4ef6\u4e2d\u5220\u9664 YAML \u914d\u7f6e\uff0c\u7136\u540e\u91cd\u65b0\u542f\u52a8Home Assistant\u4ee5\u89e3\u51b3\u6b64\u95ee\u9898\u3002", + "title": "Radarr\u7684YAML\u914d\u7f6e\u5df2\u88ab\u5220\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/zh-Hant.json b/homeassistant/components/radarr/translations/zh-Hant.json index 6b381643c1a..1c0275c23bf 100644 --- a/homeassistant/components/radarr/translations/zh-Hant.json +++ b/homeassistant/components/radarr/translations/zh-Hant.json @@ -31,9 +31,9 @@ "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Radarr \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Radarr YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Radarr YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, - "removed_attributes": { - "description": "\u5728\u8b39\u614e\u8003\u91cf\u5f8c\u3001\u95dc\u9589\u96fb\u5f71\u6578\u611f\u6e2c\u5668\u4e0a\u505a\u4e86\u4e00\u4e9b\u91cd\u5927\u8b8a\u66f4\u3002\n\n\u7531\u65bc\u5de8\u5927\u7684\u8cc7\u6599\u5eab\u53ef\u80fd\u6703\u5c0e\u81f4\u611f\u6e2c\u5668\u51fa\u73fe\u554f\u984c\u3002\u5047\u5982\u60a8\u4ecd\u60f3\u7e7c\u7e8c\u4f7f\u7528\u3001\u8acb\u6ce8\u610f\u76f8\u95dc\u554f\u984c\u3002\n\n\u96fb\u5f71\u611f\u6e2c\u5668\u5c6c\u6027\u4e2d\u5c07\u4e0d\u518d\u5305\u542b\u96fb\u5f71\u540d\u7a31\u3002\n\n\u5373\u5c07\u4e0a\u6620\u90e8\u5206\u5df2\u7d93\u79fb\u9664\u3002\u6b63\u8ddf\u8457\u884c\u4e8b\u66c6\u529f\u80fd\u9032\u884c\u66f4\u65b0\uff0c\u78c1\u789f\u7a7a\u9593\u5c07\u6703\u4f9d\u64da\u4e0d\u540c\u611f\u6e2c\u5668\u9032\u884c\u5206\u9694\u65bc\u5404\u81ea\u7684\u8cc7\u6599\u593e\u3002\n\n\u7531\u65bc\u5c0d\u65bc\u81ea\u52d5\u5316\u7684\u7528\u9014\u4e0d\u9ad8\uff0c\u72c0\u614b\u8207\u547d\u4ee4\u4e5f\u5df2\u7d93\u79fb\u9664\u3002", - "title": "\u8b8a\u66f4\u81f3 Radarr \u6574\u5408" + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Radarr \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Radarr YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" } }, "options": { diff --git a/homeassistant/components/radio_browser/translations/sk.json b/homeassistant/components/radio_browser/translations/sk.json new file mode 100644 index 00000000000..c294bc45d7c --- /dev/null +++ b/homeassistant/components/radio_browser/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/sk.json b/homeassistant/components/radiotherm/translations/sk.json new file mode 100644 index 00000000000..4595dcba837 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Chcete nastavi\u0165 {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 47bb7ce9bd9..23924bbad18 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -2,7 +2,7 @@ "domain": "rainbird", "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": ["pyrainbird==0.4.3"], + "requirements": ["pyrainbird==0.6.3"], "codeowners": ["@konikvranik"], "iot_class": "local_polling", "loggers": ["pyrainbird"] diff --git a/homeassistant/components/rainforest_eagle/translations/sk.json b/homeassistant/components/rainforest_eagle/translations/sk.json index 5ada995aa6e..9307faaa051 100644 --- a/homeassistant/components/rainforest_eagle/translations/sk.json +++ b/homeassistant/components/rainforest_eagle/translations/sk.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "cloud_id": "Cloud ID", + "host": "Hostite\u013e", + "install_code": "In\u0161tala\u010dn\u00fd k\u00f3d" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a0b11653272..321a3a057af 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -38,7 +38,9 @@ from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller from .const import ( - CONF_ZONE_RUN_TIME, + CONF_DEFAULT_ZONE_RUN_TIME, + CONF_DURATION, + CONF_USE_APP_RUN_TIMES, DATA_API_VERSIONS, DATA_MACHINE_FIRMWARE_UPDATE_STATUS, DATA_PROGRAMS, @@ -67,7 +69,6 @@ PLATFORMS = [ CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" -CONF_DURATION = "duration" CONF_ET = "et" CONF_MAXRH = "maxrh" CONF_MAXTEMP = "maxtemp" @@ -237,15 +238,17 @@ async def async_setup_entry( # noqa: C901 if not entry.unique_id or is_ip_address(entry.unique_id): # If the config entry doesn't already have a unique ID, set one: entry_updates["unique_id"] = controller.mac - if CONF_ZONE_RUN_TIME in entry.data: + if CONF_DEFAULT_ZONE_RUN_TIME in entry.data: # If a zone run time exists in the config entry's data, pop it and move it to # options: data = {**entry.data} entry_updates["data"] = data entry_updates["options"] = { **entry.options, - CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME), + CONF_DEFAULT_ZONE_RUN_TIME: data.pop(CONF_DEFAULT_ZONE_RUN_TIME), } + if CONF_USE_APP_RUN_TIMES not in entry.options: + entry_updates["options"] = {**entry.options, CONF_USE_APP_RUN_TIMES: False} if entry_updates: hass.config_entries.async_update_entry(entry, **entry_updates) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 212d87f2982..8b75a354e29 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -12,12 +12,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RainMachineData, RainMachineEntity -from .const import ( - DATA_PROVISION_SETTINGS, - DATA_RESTRICTIONS_CURRENT, - DATA_RESTRICTIONS_UNIVERSAL, - DOMAIN, -) +from .const import DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, DOMAIN from .model import ( RainMachineEntityDescription, RainMachineEntityDescriptionMixinDataKey, @@ -30,8 +25,6 @@ from .util import ( TYPE_FLOW_SENSOR = "flow_sensor" TYPE_FREEZE = "freeze" -TYPE_FREEZE_PROTECTION = "freeze_protection" -TYPE_HOT_DAYS = "extra_water_on_hot_days" TYPE_HOURLY = "hourly" TYPE_MONTH = "month" TYPE_RAINDELAY = "raindelay" @@ -64,22 +57,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( api_category=DATA_RESTRICTIONS_CURRENT, data_key="freeze", ), - RainMachineBinarySensorDescription( - key=TYPE_FREEZE_PROTECTION, - name="Freeze protection", - icon="mdi:weather-snowy", - entity_category=EntityCategory.DIAGNOSTIC, - api_category=DATA_RESTRICTIONS_UNIVERSAL, - data_key="freezeProtectEnabled", - ), - RainMachineBinarySensorDescription( - key=TYPE_HOT_DAYS, - name="Extra water on hot days", - icon="mdi:thermometer-lines", - entity_category=EntityCategory.DIAGNOSTIC, - api_category=DATA_RESTRICTIONS_UNIVERSAL, - data_key="hotDaysExtraWatering", - ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, name="Hourly restrictions", @@ -139,14 +116,14 @@ async def async_setup_entry( f"{data.controller.mac}_freeze_protection", f"switch.{data.controller.name.lower()}_freeze_protect_enabled", breaks_in_ha_version="2022.12.0", - remove_old_entity=False, + remove_old_entity=True, ), EntityDomainReplacementStrategy( BINARY_SENSOR_DOMAIN, f"{data.controller.mac}_extra_water_on_hot_days", f"switch.{data.controller.name.lower()}_hot_days_extra_watering", breaks_in_ha_version="2022.12.0", - remove_old_entity=False, + remove_old_entity=True, ), ), ) @@ -154,7 +131,6 @@ async def async_setup_entry( api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor, DATA_RESTRICTIONS_CURRENT: CurrentRestrictionsBinarySensor, - DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsBinarySensor, } async_add_entities( @@ -204,17 +180,3 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): self._attr_is_on = self.coordinator.data.get("system", {}).get( "useFlowSensor" ) - - -class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): - """Define a binary sensor that handles universal restrictions data.""" - - entity_description: RainMachineBinarySensorDescription - - @callback - def update_from_latest_data(self) -> None: - """Update the state.""" - if self.entity_description.key == TYPE_FREEZE_PROTECTION: - self._attr_is_on = self.coordinator.data.get("freezeProtectEnabled") - elif self.entity_description.key == TYPE_HOT_DAYS: - self._attr_is_on = self.coordinator.data.get("hotDaysExtraWatering") diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index eed80b9c145..b5ae42559bb 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -16,7 +16,13 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN +from .const import ( + CONF_DEFAULT_ZONE_RUN_TIME, + CONF_USE_APP_RUN_TIMES, + DEFAULT_PORT, + DEFAULT_ZONE_RUN, + DOMAIN, +) @callback @@ -138,8 +144,8 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_PORT: user_input[CONF_PORT], CONF_SSL: user_input.get(CONF_SSL, True), - CONF_ZONE_RUN_TIME: user_input.get( - CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN + CONF_DEFAULT_ZONE_RUN_TIME: user_input.get( + CONF_DEFAULT_ZONE_RUN_TIME, DEFAULT_ZONE_RUN ), }, ) @@ -173,9 +179,15 @@ class RainMachineOptionsFlowHandler(config_entries.OptionsFlow): data_schema=vol.Schema( { vol.Optional( - CONF_ZONE_RUN_TIME, - default=self.config_entry.options.get(CONF_ZONE_RUN_TIME), - ): cv.positive_int + CONF_DEFAULT_ZONE_RUN_TIME, + default=self.config_entry.options.get( + CONF_DEFAULT_ZONE_RUN_TIME + ), + ): cv.positive_int, + vol.Optional( + CONF_USE_APP_RUN_TIMES, + default=self.config_entry.options.get(CONF_USE_APP_RUN_TIMES), + ): bool, } ), ) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index d1b5bd9bd52..00af0bd0b75 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -5,7 +5,9 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "rainmachine" -CONF_ZONE_RUN_TIME = "zone_run_time" +CONF_DURATION = "duration" +CONF_DEFAULT_ZONE_RUN_TIME = "zone_run_time" +CONF_USE_APP_RUN_TIMES = "use_app_run_times" DATA_API_VERSIONS = "api.versions" DATA_MACHINE_FIRMWARE_UPDATE_STATUS = "machine.firmware_update_status" diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 191ace64625..0830fe6bc6a 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -14,20 +14,14 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS, VOLUME_LITERS +from homeassistant.const import VOLUME_CUBIC_METERS, VOLUME_LITERS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp, utcnow from . import RainMachineData, RainMachineEntity -from .const import ( - DATA_PROGRAMS, - DATA_PROVISION_SETTINGS, - DATA_RESTRICTIONS_UNIVERSAL, - DATA_ZONES, - DOMAIN, -) +from .const import DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_ZONES, DOMAIN from .model import ( RainMachineEntityDescription, RainMachineEntityDescriptionMixinDataKey, @@ -49,7 +43,6 @@ TYPE_FLOW_SENSOR_LEAK_CLICKS = "flow_sensor_leak_clicks" TYPE_FLOW_SENSOR_LEAK_VOLUME = "flow_sensor_leak_volume" TYPE_FLOW_SENSOR_START_INDEX = "flow_sensor_start_index" TYPE_FLOW_SENSOR_WATERING_CLICKS = "flow_sensor_watering_clicks" -TYPE_FREEZE_TEMP = "freeze_protect_temp" TYPE_LAST_LEAK_DETECTED = "last_leak_detected" TYPE_PROGRAM_RUN_COMPLETION_TIME = "program_run_completion_time" TYPE_RAIN_SENSOR_RAIN_START = "rain_sensor_rain_start" @@ -140,17 +133,6 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorWateringClicks", ), - RainMachineSensorDataDescription( - key=TYPE_FREEZE_TEMP, - name="Freeze protect temperature", - icon="mdi:thermometer", - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - api_category=DATA_RESTRICTIONS_UNIVERSAL, - data_key="freezeProtectTemp", - ), RainMachineSensorDataDescription( key=TYPE_LAST_LEAK_DETECTED, name="Last leak detected", @@ -191,17 +173,16 @@ async def async_setup_entry( f"{data.controller.mac}_freeze_protect_temp", f"select.{data.controller.name.lower()}_freeze_protect_temperature", breaks_in_ha_version="2022.12.0", - remove_old_entity=False, + remove_old_entity=True, ), ), ) api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsSensor, - DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsSensor, } - sensors = [ + sensors: list[ProvisionSettingsSensor | TimeRemainingSensor] = [ api_category_sensor_map[description.api_category](entry, data, description) for description in SENSOR_DESCRIPTIONS if ( @@ -373,18 +354,6 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): self._attr_native_value = new_value -class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity): - """Define a sensor that handles universal restrictions data.""" - - entity_description: RainMachineSensorDataDescription - - @callback - def update_from_latest_data(self) -> None: - """Update the state.""" - if self.entity_description.key == TYPE_FREEZE_TEMP: - self._attr_native_value = self.coordinator.data.get("freezeProtectTemp") - - class ZoneTimeRemainingSensor(TimeRemainingSensor): """Define a sensor that shows the amount of time remaining for a zone.""" diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 7634c0a69c5..9991fd31e03 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -23,7 +23,8 @@ "init": { "title": "Configure RainMachine", "data": { - "zone_run_time": "Default zone run time (in seconds)" + "zone_run_time": "Default zone run time (in seconds)", + "use_app_run_times": "Use zone run times from RainMachine app" } } } diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 39087f36660..66081588f27 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -22,8 +22,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RainMachineData, RainMachineEntity, async_update_programs_and_zones from .const import ( - CONF_ZONE_RUN_TIME, + CONF_DEFAULT_ZONE_RUN_TIME, + CONF_DURATION, + CONF_USE_APP_RUN_TIMES, DATA_PROGRAMS, + DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, DEFAULT_ZONE_RUN, @@ -40,6 +43,7 @@ ATTR_AREA = "area" ATTR_CS_ON = "cs_on" ATTR_CURRENT_CYCLE = "current_cycle" ATTR_CYCLES = "cycles" +ATTR_ZONE_RUN_TIME = "zone_run_time_from_app" ATTR_DELAY = "delay" ATTR_DELAY_ON = "delay_on" ATTR_FIELD_CAPACITY = "field_capacity" @@ -186,7 +190,7 @@ async def async_setup_entry( "start_zone", { vol.Optional( - CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN + CONF_DEFAULT_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN ): cv.positive_int }, "async_start_zone", @@ -460,9 +464,21 @@ class RainMachineZone(RainMachineActivitySwitch): @raise_on_request_error async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" + # 1. Use duration parameter if provided from service call + duration = kwargs.get(CONF_DURATION) + if not duration: + if ( + self._entry.options[CONF_USE_APP_RUN_TIMES] + and ATTR_ZONE_RUN_TIME in self._attr_extra_state_attributes + ): + # 2. Use app's zone-specific default, if enabled and available + duration = self._attr_extra_state_attributes[ATTR_ZONE_RUN_TIME] + else: + # 3. Fall back to global zone default duration + duration = self._entry.options[CONF_DEFAULT_ZONE_RUN_TIME] await self._data.controller.zones.start( self.entity_description.uid, - kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), + duration, ) self._update_activities() @@ -498,6 +514,13 @@ class RainMachineZone(RainMachineActivitySwitch): data["waterSense"]["precipitationRate"], 2 ) + if self._entry.options[CONF_USE_APP_RUN_TIMES]: + provision_data = self._data.coordinators[DATA_PROVISION_SETTINGS].data + if zone_durations := provision_data.get("system", {}).get("zoneDuration"): + attrs[ATTR_ZONE_RUN_TIME] = zone_durations[ + list(self.coordinator.data).index(self.entity_description.uid) + ] + self._attr_extra_state_attributes.update(attrs) diff --git a/homeassistant/components/rainmachine/translations/bg.json b/homeassistant/components/rainmachine/translations/bg.json index 4ba51cf991e..1239915231b 100644 --- a/homeassistant/components/rainmachine/translations/bg.json +++ b/homeassistant/components/rainmachine/translations/bg.json @@ -11,17 +11,5 @@ "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0441\u0438" } } - }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" - } - } - }, - "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" - } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/ca.json b/homeassistant/components/rainmachine/translations/ca.json index e776a0bcdf3..ecace654255 100644 --- a/homeassistant/components/rainmachine/translations/ca.json +++ b/homeassistant/components/rainmachine/translations/ca.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat perqu\u00e8 passin a utilitzar l'entitat `{replacement_entity_id}`.", - "title": "L'entitat {old_entity_id} s'eliminar\u00e0" - } - } - }, - "title": "L'entitat {old_entity_id} s'eliminar\u00e0" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Utilitza els temps d'execuci\u00f3 de zona de l'aplicaci\u00f3 RainMachine", "zone_run_time": "Temps d'execuci\u00f3 predeterminat de la zona (en segons)" }, "title": "Configuraci\u00f3 de RainMachine" diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 51b6f0814af..74416d0165c 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t verwenden, um stattdessen `{replacement_entity_id}` zu verwenden.", - "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" - } - } - }, - "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Zonenlaufzeiten aus der RainMachine App verwenden", "zone_run_time": "Standard-Zonenlaufzeit (in Sekunden)" }, "title": "RainMachine konfigurieren" diff --git a/homeassistant/components/rainmachine/translations/el.json b/homeassistant/components/rainmachine/translations/el.json index 313f2bfc1fb..a1811b90057 100644 --- a/homeassistant/components/rainmachine/translations/el.json +++ b/homeassistant/components/rainmachine/translations/el.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03bf `{replacement_entity_id}`.", - "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" - } - } - }, - "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5\u03c2 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae RainMachine", "zone_run_time": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2 (\u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 RainMachine" diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index 3e5d824ee08..701a338e88d 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", - "title": "The {old_entity_id} entity will be removed" - } - } - }, - "title": "The {old_entity_id} entity will be removed" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Use zone run times from RainMachine app", "zone_run_time": "Default zone run time (in seconds)" }, "title": "Configure RainMachine" diff --git a/homeassistant/components/rainmachine/translations/es.json b/homeassistant/components/rainmachine/translations/es.json index a8bcc7cbd0d..22726b4bf40 100644 --- a/homeassistant/components/rainmachine/translations/es.json +++ b/homeassistant/components/rainmachine/translations/es.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad para usar `{replacement_entity_id}`.", - "title": "Se eliminar\u00e1 la entidad {old_entity_id}" - } - } - }, - "title": "Se eliminar\u00e1 la entidad {old_entity_id}" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Utilizar los tiempos de ejecuci\u00f3n de las zonas desde la aplicaci\u00f3n RainMachine", "zone_run_time": "Tiempo de ejecuci\u00f3n de zona predeterminado (en segundos)" }, "title": "Configurar RainMachine" diff --git a/homeassistant/components/rainmachine/translations/et.json b/homeassistant/components/rainmachine/translations/et.json index 0a9d6e007f1..1f45d8d3c30 100644 --- a/homeassistant/components/rainmachine/translations/et.json +++ b/homeassistant/components/rainmachine/translations/et.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "V\u00e4rskendage automaatikaid v\u00f5i skripte, mis seda olemit kasutavad, et kasutada selle asemel '{replacement_entity_id}'.", - "title": "\u00dcksus {old_entity_id} eemaldatakse" - } - } - }, - "title": "\u00dcksus {old_entity_id} eemaldatakse" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Kasuta tsoonide t\u00f6\u00f6aegu rakendusest RainMachine", "zone_run_time": "Vaiketsooni k\u00e4itamisaeg (sekundites)" }, "title": "Seadista RainMachine" diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index 63f5af55527..d9a6c011755 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -18,19 +18,6 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Modifiez tout script ou automatisation utilisant cette entit\u00e9 afin qu'ils utilisent `{replacement_entity_id}` \u00e0 la place.", - "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" - } - } - }, - "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index f86c3905108..419ab2cf4b0 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Friss\u00edtse az ezt az entit\u00e1st haszn\u00e1l\u00f3 automatiz\u00e1l\u00e1sokat vagy szkripteket, hogy helyette a k\u00f6vetkez\u0151t haszn\u00e1ja: `{replacement_entity_id}`", - "title": "{old_entity_id} entit\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl." - } - } - }, - "title": "{old_entity_id} entit\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl." - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Z\u00f3na fut\u00e1si id\u0151k haszn\u00e1lata a RainMachine alkalmaz\u00e1sb\u00f3l", "zone_run_time": "Alap\u00e9rtelmezett z\u00f3nafut\u00e1si id\u0151 (m\u00e1sodpercben)" }, "title": "RainMachine konfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/rainmachine/translations/id.json b/homeassistant/components/rainmachine/translations/id.json index 8223fb4a792..03ea324b62b 100644 --- a/homeassistant/components/rainmachine/translations/id.json +++ b/homeassistant/components/rainmachine/translations/id.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Perbarui semua otomasi atau skrip yang menggunakan entitas ini untuk menggunakan `{replacement_entity_id}`.", - "title": "Entitas {old_entity_id} akan dihapus" - } - } - }, - "title": "Entitas {old_entity_id} akan dihapus" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Gunakan waktu berjalan zona dari aplikasi RainMachine", "zone_run_time": "Waktu berjalan zona default (dalam detik)" }, "title": "Konfigurasikan RainMachine" diff --git a/homeassistant/components/rainmachine/translations/it.json b/homeassistant/components/rainmachine/translations/it.json index 9cca839ea00..da26d3f3c65 100644 --- a/homeassistant/components/rainmachine/translations/it.json +++ b/homeassistant/components/rainmachine/translations/it.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aggiorna tutte le automazioni o gli script che utilizzano questa entit\u00e0 in modo che utilizzino invece `{replacement_entity_id}`.", - "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" - } - } - }, - "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Usa i tempi di esecuzione delle zone dall'app RainMachine", "zone_run_time": "Tempo di esecuzione della zona di default (in secondi)" }, "title": "Configura RainMachine" diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index 4fa3285c03b..cbf76b879cb 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -18,18 +18,6 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "title": "De {old_entity_id}-entiteit wordt verwijderd" - } - } - }, - "title": "De {old_entity_id}-entiteit wordt verwijderd" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index f7e0a758ee0..180674b0bb4 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten til i stedet \u00e5 bruke ` {replacement_entity_id} `.", - "title": "{old_entity_id} vil bli fjernet" - } - } - }, - "title": "{old_entity_id} vil bli fjernet" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Bruk sonekj\u00f8ringstider fra RainMachine-appen", "zone_run_time": "Standard sonekj\u00f8ringstid (i sekunder)" }, "title": "Konfigurer RainMachine" diff --git a/homeassistant/components/rainmachine/translations/pl.json b/homeassistant/components/rainmachine/translations/pl.json index e96ccd64ef0..36f3531a0a0 100644 --- a/homeassistant/components/rainmachine/translations/pl.json +++ b/homeassistant/components/rainmachine/translations/pl.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej encji, aby zamiast tego u\u017cywa\u0142y `{replacement_entity_id}`.", - "title": "Encja {old_entity_id} zostanie usuni\u0119ta" - } - } - }, - "title": "Encja {old_entity_id} zostanie usuni\u0119ta" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "U\u017cyj czas\u00f3w dla strefy z aplikacji RainMachine", "zone_run_time": "Domy\u015blny czas dzia\u0142ania dla strefy (w sekundach)" }, "title": "Konfiguracja RainMachine" diff --git a/homeassistant/components/rainmachine/translations/pt-BR.json b/homeassistant/components/rainmachine/translations/pt-BR.json index 7128423202e..25662e1004d 100644 --- a/homeassistant/components/rainmachine/translations/pt-BR.json +++ b/homeassistant/components/rainmachine/translations/pt-BR.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam essa entidade para usar `{replacement_entity_id}`.", - "title": "A entidade {old_entity_id} ser\u00e1 removida" - } - } - }, - "title": "A entidade {old_entity_id} ser\u00e1 removida" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Use os tempos de execu\u00e7\u00e3o da zona do aplicativo RainMachine", "zone_run_time": "Tempo de execu\u00e7\u00e3o da zona padr\u00e3o (em segundos)" }, "title": "Configurar RainMachine" diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 2ed8c6df530..28d82d0b24c 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "ip_address": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442" }, @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 `{replacement_entity_id}`.", - "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" - } - } - }, - "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0437\u043e\u043d \u0438\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f RainMachine", "zone_run_time": "\u0412\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0437\u043e\u043d\u044b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 RainMachine" diff --git a/homeassistant/components/rainmachine/translations/sk.json b/homeassistant/components/rainmachine/translations/sk.json index 7fd0d4942e8..351e0cf97be 100644 --- a/homeassistant/components/rainmachine/translations/sk.json +++ b/homeassistant/components/rainmachine/translations/sk.json @@ -1,14 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, + "flow_title": "{ip}", "step": { "user": { "data": { + "ip_address": "N\u00e1zov hostite\u013ea alebo IP adresa", "password": "Heslo", "port": "Port" - } + }, + "title": "Vypl\u0148te svoje \u00fadaje" } } } diff --git a/homeassistant/components/rainmachine/translations/sv.json b/homeassistant/components/rainmachine/translations/sv.json index 9cf860dee34..10e06693207 100644 --- a/homeassistant/components/rainmachine/translations/sv.json +++ b/homeassistant/components/rainmachine/translations/sv.json @@ -18,19 +18,6 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Uppdatera alla automatiseringar eller skript som anv\u00e4nder denna enhet f\u00f6r att ist\u00e4llet anv\u00e4nda ` {replacement_entity_id} `.", - "title": "{old_entity_id} kommer att tas bort" - } - } - }, - "title": "{old_entity_id} kommer att tas bort" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/tr.json b/homeassistant/components/rainmachine/translations/tr.json index fa181de3505..011b4fdd0a3 100644 --- a/homeassistant/components/rainmachine/translations/tr.json +++ b/homeassistant/components/rainmachine/translations/tr.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Bunun yerine ` {replacement_entity_id} ` kullanmak i\u00e7in bu varl\u0131\u011f\u0131 kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 g\u00fcncelleyin.", - "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" - } - } - }, - "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "RainMachine uygulamas\u0131ndan b\u00f6lge \u00e7al\u0131\u015ft\u0131rma s\u00fcrelerini kullan\u0131n", "zone_run_time": "Varsay\u0131lan b\u00f6lge \u00e7al\u0131\u015fma s\u00fcresi (saniye cinsinden)" }, "title": "RainMachine'i konf\u0131g\u00fcre et" diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index c0ead98a13e..886840b2c68 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -18,23 +18,11 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\uff0c\u4ee5\u53d6\u4ee3 `{replacement_entity_id}`\u3002", - "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" - } - } - }, - "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" - } - }, "options": { "step": { "init": { "data": { + "use_app_run_times": "Use zone run times \u4f7f\u7528 RainMachine App \u4e2d\u7684\u5340\u57df\u57f7\u884c\u6b21\u6578", "zone_run_time": "\u9810\u8a2d\u5340\u57df\u57f7\u884c\u6642\u9593\uff08\u79d2\uff09" }, "title": "\u8a2d\u5b9a RainMachine" diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index 6433b15adb5..61417f751ac 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -42,6 +42,10 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: if not board.startswith("rpi"): raise HomeAssistantError + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + return [ HardwareInfo( board=BoardInfo( @@ -50,6 +54,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: model=MODELS.get(board), revision=None, ), + config_entries=config_entries, dongle=None, name=BOARD_NAMES.get(board, f"Unknown Raspberry Pi model '{board}'"), url=None, diff --git a/homeassistant/components/rdw/translations/sk.json b/homeassistant/components/rdw/translations/sk.json new file mode 100644 index 00000000000..d6fa5e791b4 --- /dev/null +++ b/homeassistant/components/rdw/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/sk.json b/homeassistant/components/recollect_waste/translations/sk.json new file mode 100644 index 00000000000..09913d47d95 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "invalid_place_or_service_id": "Neplatn\u00e9 ID miesta alebo slu\u017eby" + }, + "step": { + "user": { + "data": { + "service_id": "ID slu\u017eby" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 0d4bfe8e59b..3e1e8264642 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -21,10 +21,12 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import statistics, websocket_api -from .const import ( +from .const import ( # noqa: F401 CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, + EVENT_RECORDER_5MIN_STATISTICS_GENERATED, + EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, EXCLUDE_ATTRIBUTES, SQLITE_URL_PREFIX, ) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 66a9818b4b8..8db4b43e04e 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -14,6 +14,9 @@ MYSQLDB_URL_PREFIX = "mysql://" MYSQLDB_PYMYSQL_URL_PREFIX = "mysql+pymysql://" DOMAIN = "recorder" +EVENT_RECORDER_5MIN_STATISTICS_GENERATED = "recorder_5min_statistics_generated" +EVENT_RECORDER_HOURLY_STATISTICS_GENERATED = "recorder_hourly_statistics_generated" + CONF_DB_INTEGRITY_CHECK = "db_integrity_check" MAX_QUEUE_BACKLOG = 40000 diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 0ef8c20d5c7..a79724f765a 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -375,12 +375,6 @@ class Recorder(threading.Thread): # Unknown what it is. return True - def do_adhoc_statistics(self, **kwargs: Any) -> None: - """Trigger an adhoc statistics run.""" - if not (start := kwargs.get("start")): - start = statistics.get_start_time() - self.queue_task(StatisticsTask(start)) - def _empty_queue(self, event: Event) -> None: """Empty the queue if its still present at final write.""" @@ -479,7 +473,7 @@ class Recorder(threading.Thread): Short term statistics run every 5 minutes """ start = statistics.get_start_time() - self.queue_task(StatisticsTask(start)) + self.queue_task(StatisticsTask(start, True)) @callback def async_adjust_statistics( @@ -597,16 +591,14 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_connection_failed) return - schema_status = migration.validate_db_schema(self.hass, self.get_session) + schema_status = migration.validate_db_schema(self.hass, self, self.get_session) if schema_status is None: # Give up if we could not validate the schema self.hass.add_job(self.async_connection_failed) return self.schema_version = schema_status.current_version - schema_is_valid = migration.schema_is_valid(schema_status) - - if schema_is_valid: + if schema_status.valid: self._setup_run() else: self.migration_in_progress = True @@ -614,8 +606,8 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_connection_success) - if self.migration_is_live or schema_is_valid: - # If the migrate is live or the schema is current, we need to + if self.migration_is_live or schema_status.valid: + # If the migrate is live or the schema is valid, we need to # wait for startup to complete. If its not live, we need to continue # on. self.hass.add_job(self.async_set_db_ready) @@ -632,7 +624,7 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_set_db_ready) return - if not schema_is_valid: + if not schema_status.valid: if self._migrate_schema_and_setup_run(schema_status): self.schema_version = SCHEMA_VERSION if not self._event_listener: @@ -1193,7 +1185,7 @@ class Recorder(threading.Thread): while start < last_period: end = start + timedelta(minutes=5) _LOGGER.debug("Compiling missing statistics for %s-%s", start, end) - self.queue_task(StatisticsTask(start)) + self.queue_task(StatisticsTask(start, end >= last_period)) start = end def _end_session(self) -> None: diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 22a3b382c7d..28952f127e2 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable import contextlib -from dataclasses import dataclass +from dataclasses import dataclass, replace as dataclass_replace from datetime import timedelta import logging from typing import TYPE_CHECKING @@ -37,9 +37,11 @@ from .db_schema import ( ) from .models import process_timestamp from .statistics import ( + correct_db_schema as statistics_correct_db_schema, delete_statistics_duplicates, delete_statistics_meta_duplicates, get_start_time, + validate_db_schema as statistics_validate_db_schema, ) from .util import session_scope @@ -83,6 +85,8 @@ class SchemaValidationStatus: """Store schema validation status.""" current_version: int + statistics_schema_errors: set[str] + valid: bool def _schema_is_current(current_version: int) -> bool: @@ -90,13 +94,8 @@ def _schema_is_current(current_version: int) -> bool: return current_version == SCHEMA_VERSION -def schema_is_valid(schema_status: SchemaValidationStatus) -> bool: - """Check if the schema is valid.""" - return _schema_is_current(schema_status.current_version) - - def validate_db_schema( - hass: HomeAssistant, session_maker: Callable[[], Session] + hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session] ) -> SchemaValidationStatus | None: """Check if the schema is valid. @@ -104,11 +103,20 @@ def validate_db_schema( errors caused by manual migration between database engines, for example importing an SQLite database to MariaDB. """ + schema_errors: set[str] = set() + current_version = get_schema_version(session_maker) if current_version is None: return None - return SchemaValidationStatus(current_version) + if is_current := _schema_is_current(current_version): + # We can only check for further errors if the schema is current, because + # columns may otherwise not exist etc. + schema_errors |= statistics_validate_db_schema(hass, engine, session_maker) + + valid = is_current and not schema_errors + + return SchemaValidationStatus(current_version, schema_errors, valid) def live_migration(schema_status: SchemaValidationStatus) -> bool: @@ -125,10 +133,18 @@ def migrate_schema( ) -> None: """Check if the schema needs to be upgraded.""" current_version = schema_status.current_version - _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) + if current_version != SCHEMA_VERSION: + _LOGGER.warning( + "Database is about to upgrade from schema version: %s to: %s", + current_version, + SCHEMA_VERSION, + ) db_ready = False for version in range(current_version, SCHEMA_VERSION): - if live_migration(SchemaValidationStatus(version)) and not db_ready: + if ( + live_migration(dataclass_replace(schema_status, current_version=version)) + and not db_ready + ): db_ready = True instance.migration_is_live = True hass.add_job(instance.async_set_db_ready) @@ -140,6 +156,13 @@ def migrate_schema( _LOGGER.info("Upgrade to version %s done", new_version) + if schema_errors := schema_status.statistics_schema_errors: + _LOGGER.warning( + "Database is about to correct DB schema errors: %s", + ", ".join(sorted(schema_errors)), + ) + statistics_correct_db_schema(instance, engine, session_maker, schema_errors) + def _create_index( session_maker: Callable[[], Session], table_name: str, index_name: str diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index cfc797cf7ea..48b45b4da2e 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,9 +1,9 @@ """Models for Recorder.""" from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta import logging -from typing import Any, TypedDict, overload +from typing import Any, Literal, TypedDict, overload from sqlalchemy.engine.row import Row @@ -160,7 +160,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore[override] + @property def context(self) -> Context: """State context.""" if self._context is None: @@ -172,7 +172,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore[override] + @property def last_changed(self) -> datetime: """Last changed datetime.""" if self._last_changed is None: @@ -187,7 +187,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore[override] + @property def last_updated(self) -> datetime: """Last updated datetime.""" if self._last_updated is None: @@ -284,3 +284,32 @@ def row_to_compressed_state( row_changed_changed ) return comp_state + + +class CalendarStatisticPeriod(TypedDict, total=False): + """Statistic period definition.""" + + period: Literal["hour", "day", "week", "month", "year"] + offset: int + + +class FixedStatisticPeriod(TypedDict, total=False): + """Statistic period definition.""" + + end_time: datetime + start_time: datetime + + +class RollingWindowStatisticPeriod(TypedDict, total=False): + """Statistic period definition.""" + + duration: timedelta + offset: timedelta + + +class StatisticPeriod(TypedDict, total=False): + """Statistic period definition.""" + + calendar: CalendarStatisticPeriod + fixed_period: FixedStatisticPeriod + rolling_window: RollingWindowStatisticPeriod diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index e361249580f..dea454a5a33 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Mapping import contextlib import dataclasses from datetime import datetime, timedelta @@ -15,9 +15,10 @@ import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal -from sqlalchemy import bindparam, func, lambda_stmt, select +from sqlalchemy import bindparam, func, lambda_stmt, select, text +from sqlalchemy.engine import Engine from sqlalchemy.engine.row import Row -from sqlalchemy.exc import SQLAlchemyError, StatementError +from sqlalchemy.exc import OperationalError, SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true from sqlalchemy.sql.lambdas import StatementLambdaElement @@ -45,7 +46,13 @@ from homeassistant.util.unit_conversion import ( VolumeConverter, ) -from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect +from .const import ( + DOMAIN, + EVENT_RECORDER_5MIN_STATISTICS_GENERATED, + EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, + MAX_ROWS_TO_PURGE, + SupportedDialect, +) from .db_schema import ( Statistics, StatisticsBase, @@ -53,13 +60,7 @@ from .db_schema import ( StatisticsRuns, StatisticsShortTerm, ) -from .models import ( - StatisticData, - StatisticMetaData, - StatisticResult, - process_timestamp, - process_timestamp_to_utc_isoformat, -) +from .models import StatisticData, StatisticMetaData, StatisticResult, process_timestamp from .util import ( execute, execute_stmt_lambda_element, @@ -147,6 +148,30 @@ def _get_unit_class(unit: str | None) -> str | None: return None +def get_display_unit( + hass: HomeAssistant, + statistic_id: str, + statistic_unit: str | None, +) -> str | None: + """Return the unit which the statistic will be displayed in.""" + + if statistic_unit is None: + return None + + if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: + return statistic_unit + + state_unit: str | None = statistic_unit + if state := hass.states.get(statistic_id): + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if state_unit == statistic_unit or state_unit not in converter.VALID_UNITS: + # Guard against invalid state unit in the DB + return statistic_unit + + return state_unit + + def _get_statistic_to_display_unit_converter( statistic_unit: str | None, state_unit: str | None, @@ -646,7 +671,7 @@ def _compile_hourly_statistics(session: Session, start: datetime) -> None: @retryable_database_job("statistics") -def compile_statistics(instance: Recorder, start: datetime) -> bool: +def compile_statistics(instance: Recorder, start: datetime, fire_events: bool) -> bool: """Compile 5-minute statistics for all integrations with a recorder platform. The actual calculation is delegated to the platforms. @@ -702,6 +727,11 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: session.add(StatisticsRuns(start=start)) + if fire_events: + instance.hass.bus.fire(EVENT_RECORDER_5MIN_STATISTICS_GENERATED) + if start.minute == 55: + instance.hass.bus.fire(EVENT_RECORDER_HOURLY_STATISTICS_GENERATED) + return True @@ -845,12 +875,17 @@ def get_metadata( ) +def _clear_statistics_with_session(session: Session, statistic_ids: list[str]) -> None: + """Clear statistics for a list of statistic_ids.""" + session.query(StatisticsMeta).filter( + StatisticsMeta.statistic_id.in_(statistic_ids) + ).delete(synchronize_session=False) + + def clear_statistics(instance: Recorder, statistic_ids: list[str]) -> None: """Clear statistics for a list of statistic_ids.""" with session_scope(session=instance.get_session()) as session: - session.query(StatisticsMeta).filter( - StatisticsMeta.statistic_id.in_(statistic_ids) - ).delete(synchronize_session=False) + _clear_statistics_with_session(session, statistic_ids) def update_statistics_metadata( @@ -897,6 +932,9 @@ def list_statistic_ids( result = { meta["statistic_id"]: { + "display_unit_of_measurement": get_display_unit( + hass, meta["statistic_id"], meta["unit_of_measurement"] + ), "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -919,6 +957,7 @@ def list_statistic_ids( if key in result: continue result[key] = { + "display_unit_of_measurement": meta["unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -931,6 +970,7 @@ def list_statistic_ids( return [ { "statistic_id": _id, + "display_unit_of_measurement": info["display_unit_of_measurement"], "has_mean": info["has_mean"], "has_sum": info["has_sum"], "name": info.get("name"), @@ -947,6 +987,7 @@ def _reduce_statistics( same_period: Callable[[datetime, datetime], bool], period_start_end: Callable[[datetime], tuple[datetime, datetime]], period: timedelta, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict[str, Any]]]: """Reduce hourly statistics to daily or monthly statistics.""" result: dict[str, list[dict[str, Any]]] = defaultdict(list) @@ -963,19 +1004,24 @@ def _reduce_statistics( if not same_period(prev_stat["start"], statistic["start"]): start, end = period_start_end(prev_stat["start"]) # The previous statistic was the last entry of the period - result[statistic_id].append( - { - "statistic_id": statistic_id, - "start": start.isoformat(), - "end": end.isoformat(), - "mean": mean(mean_values) if mean_values else None, - "min": min(min_values) if min_values else None, - "max": max(max_values) if max_values else None, - "last_reset": prev_stat.get("last_reset"), - "state": prev_stat.get("state"), - "sum": prev_stat["sum"], - } - ) + row: dict[str, Any] = { + "start": start, + "end": end, + } + if "mean" in types: + row["mean"] = mean(mean_values) if mean_values else None + if "min" in types: + row["min"] = min(min_values) if min_values else None + if "max" in types: + row["max"] = max(max_values) if max_values else None + if "last_reset" in types: + row["last_reset"] = prev_stat.get("last_reset") + if "state" in types: + row["state"] = prev_stat.get("state") + if "sum" in types: + row["sum"] = prev_stat["sum"] + result[statistic_id].append(row) + max_values = [] mean_values = [] min_values = [] @@ -1007,11 +1053,12 @@ def day_start_end(time: datetime) -> tuple[datetime, datetime]: def _reduce_statistics_per_day( - stats: dict[str, list[dict[str, Any]]] + stats: dict[str, list[dict[str, Any]]], + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict[str, Any]]]: """Reduce hourly statistics to daily statistics.""" - return _reduce_statistics(stats, same_day, day_start_end, timedelta(days=1)) + return _reduce_statistics(stats, same_day, day_start_end, timedelta(days=1), types) def same_week(time1: datetime, time2: datetime) -> bool: @@ -1037,10 +1084,13 @@ def week_start_end(time: datetime) -> tuple[datetime, datetime]: def _reduce_statistics_per_week( stats: dict[str, list[dict[str, Any]]], + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict[str, Any]]]: """Reduce hourly statistics to weekly statistics.""" - return _reduce_statistics(stats, same_week, week_start_end, timedelta(days=7)) + return _reduce_statistics( + stats, same_week, week_start_end, timedelta(days=7), types + ) def same_month(time1: datetime, time2: datetime) -> bool: @@ -1063,53 +1113,47 @@ def month_start_end(time: datetime) -> tuple[datetime, datetime]: def _reduce_statistics_per_month( stats: dict[str, list[dict[str, Any]]], + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict[str, Any]]]: """Reduce hourly statistics to monthly statistics.""" - return _reduce_statistics(stats, same_month, month_start_end, timedelta(days=31)) + return _reduce_statistics( + stats, same_month, month_start_end, timedelta(days=31), types + ) def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, + table: type[Statistics | StatisticsShortTerm], + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> StatementLambdaElement: """Prepare a database query for statistics during a given period. This prepares a lambda_stmt query, so we don't insert the parameters yet. """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) - ) + + columns = [table.metadata_id, table.start] + if "last_reset" in types: + columns.append(table.last_reset) + if "max" in types: + columns.append(table.max) + if "mean" in types: + columns.append(table.mean) + if "min" in types: + columns.append(table.min) + if "state" in types: + columns.append(table.state) + if "sum" in types: + columns.append(table.sum) + + stmt = lambda_stmt(lambda: select(columns).filter(table.start >= start_time)) if end_time is not None: - stmt += lambda q: q.filter(Statistics.start < end_time) + stmt += lambda q: q.filter(table.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) - return stmt - - -def _statistics_during_period_stmt_short_term( - start_time: datetime, - end_time: datetime | None, - metadata_ids: list[int] | None, -) -> StatementLambdaElement: - """Prepare a database query for short term statistics during a given period. - - This prepares a lambda_stmt query, so we don't insert the parameters yet. - """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( - StatisticsShortTerm.start >= start_time - ) - ) - if end_time is not None: - stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) - if metadata_ids: - stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by( - StatisticsShortTerm.metadata_id, StatisticsShortTerm.start - ) + stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by(table.metadata_id, table.start) return stmt @@ -1119,7 +1163,7 @@ def _get_max_mean_min_statistic_in_sub_period( start_time: datetime | None, end_time: datetime | None, table: type[Statistics | StatisticsShortTerm], - types: set[str], + types: set[Literal["max", "mean", "min", "change"]], metadata_id: int, ) -> None: """Return max, mean and min during the period.""" @@ -1160,7 +1204,7 @@ def _get_max_mean_min_statistic( tail_end_time: datetime | None, tail_only: bool, metadata_id: int, - types: set[str], + types: set[Literal["max", "mean", "min", "change"]], ) -> dict[str, float | None]: """Return max, mean and min during the period. @@ -1380,7 +1424,7 @@ def statistic_during_period( start_time: datetime | None, end_time: datetime | None, statistic_id: str, - types: set[str] | None, + types: set[Literal["max", "mean", "min", "change"]] | None, units: dict[str, str] | None, ) -> dict[str, Any]: """Return a statistic data point for the UTC period start_time - end_time.""" @@ -1464,13 +1508,6 @@ def statistic_during_period( main_start_time = start_time if head_end_time is None else head_end_time main_end_time = end_time if tail_start_time is None else tail_start_time - # Fetch metadata for the given statistic_id - metadata = get_metadata_with_session(session, statistic_ids=[statistic_id]) - if not metadata: - return result - - metadata_id = metadata[statistic_id][0] - if not types.isdisjoint({"max", "mean", "min"}): result = _get_max_mean_min_statistic( session, @@ -1531,14 +1568,15 @@ def statistic_during_period( return {key: convert(value) for key, value in result.items()} -def statistics_during_period( +def _statistics_during_period_with_session( hass: HomeAssistant, + session: Session, start_time: datetime, - end_time: datetime | None = None, - statistic_ids: list[str] | None = None, - period: Literal["5minute", "day", "hour", "week", "month"] = "hour", - start_time_as_datetime: bool = False, - units: dict[str, str] | None = None, + end_time: datetime | None, + statistic_ids: list[str] | None, + period: Literal["5minute", "day", "hour", "week", "month"], + units: dict[str, str] | None, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict[str, Any]]]: """Return statistic data points during UTC period start_time - end_time. @@ -1546,44 +1584,28 @@ def statistics_during_period( If statistic_ids is omitted, returns statistics for all statistics ids. """ metadata = None - with session_scope(hass=hass) as session: - # Fetch metadata for the given (or all) statistic_ids - metadata = get_metadata_with_session(session, statistic_ids=statistic_ids) - if not metadata: - return {} + # Fetch metadata for the given (or all) statistic_ids + metadata = get_metadata_with_session(session, statistic_ids=statistic_ids) + if not metadata: + return {} - metadata_ids = None - if statistic_ids is not None: - metadata_ids = [metadata_id for metadata_id, _ in metadata.values()] + metadata_ids = None + if statistic_ids is not None: + metadata_ids = [metadata_id for metadata_id, _ in metadata.values()] - if period == "5minute": - table = StatisticsShortTerm - stmt = _statistics_during_period_stmt_short_term( - start_time, end_time, metadata_ids - ) - else: - table = Statistics - stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) - stats = execute_stmt_lambda_element(session, stmt) + table: type[Statistics | StatisticsShortTerm] = ( + Statistics if period != "5minute" else StatisticsShortTerm + ) + stmt = _statistics_during_period_stmt( + start_time, end_time, metadata_ids, table, types + ) + stats = execute_stmt_lambda_element(session, stmt) - if not stats: - return {} - # Return statistics combined with metadata - if period not in ("day", "week", "month"): - return _sorted_statistics_to_dict( - hass, - session, - stats, - statistic_ids, - metadata, - True, - table, - start_time, - start_time_as_datetime, - units, - ) - - result = _sorted_statistics_to_dict( + if not stats: + return {} + # Return statistics combined with metadata + if period not in ("day", "week", "month"): + return _sorted_statistics_to_dict( hass, session, stats, @@ -1592,17 +1614,57 @@ def statistics_during_period( True, table, start_time, - True, units, + types, ) - if period == "day": - return _reduce_statistics_per_day(result) + result = _sorted_statistics_to_dict( + hass, + session, + stats, + statistic_ids, + metadata, + True, + table, + start_time, + units, + types, + ) - if period == "week": - return _reduce_statistics_per_week(result) + if period == "day": + return _reduce_statistics_per_day(result, types) - return _reduce_statistics_per_month(result) + if period == "week": + return _reduce_statistics_per_week(result, types) + + return _reduce_statistics_per_month(result, types) + + +def statistics_during_period( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None, + statistic_ids: list[str] | None, + period: Literal["5minute", "day", "hour", "week", "month"], + units: dict[str, str] | None, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], +) -> dict[str, list[dict[str, Any]]]: + """Return statistic data points during UTC period start_time - end_time. + + If end_time is omitted, returns statistics newer than or equal to start_time. + If statistic_ids is omitted, returns statistics for all statistics ids. + """ + with session_scope(hass=hass) as session: + return _statistics_during_period_with_session( + hass, + session, + start_time, + end_time, + statistic_ids, + period, + units, + types, + ) def _get_last_statistics_stmt( @@ -1637,6 +1699,7 @@ def _get_last_statistics( statistic_id: str, convert_units: bool, table: type[Statistics | StatisticsShortTerm], + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict]]: """Return the last number_of_stats statistics for a given statistic_id.""" statistic_ids = [statistic_id] @@ -1665,26 +1728,34 @@ def _get_last_statistics( convert_units, table, None, - False, None, + types, ) def get_last_statistics( - hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool + hass: HomeAssistant, + number_of_stats: int, + statistic_id: str, + convert_units: bool, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict]]: """Return the last number_of_stats statistics for a statistic_id.""" return _get_last_statistics( - hass, number_of_stats, statistic_id, convert_units, Statistics + hass, number_of_stats, statistic_id, convert_units, Statistics, types ) def get_last_short_term_statistics( - hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool + hass: HomeAssistant, + number_of_stats: int, + statistic_id: str, + convert_units: bool, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict]]: """Return the last number_of_stats short term statistics for a statistic_id.""" return _get_last_statistics( - hass, number_of_stats, statistic_id, convert_units, StatisticsShortTerm + hass, number_of_stats, statistic_id, convert_units, StatisticsShortTerm, types ) @@ -1720,6 +1791,7 @@ def _latest_short_term_statistics_stmt( def get_latest_short_term_statistics( hass: HomeAssistant, statistic_ids: list[str], + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], metadata: dict[str, tuple[int, StatisticMetaData]] | None = None, ) -> dict[str, list[dict]]: """Return the latest short term statistics for a list of statistic_ids.""" @@ -1749,8 +1821,8 @@ def get_latest_short_term_statistics( False, StatisticsShortTerm, None, - False, None, + types, ) @@ -1759,31 +1831,38 @@ def _statistics_at_time( metadata_ids: set[int], table: type[Statistics | StatisticsShortTerm], start_time: datetime, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> list | None: """Return last known statistics, earlier than start_time, for the metadata_ids.""" - # Fetch metadata for the given (or all) statistic_ids - if table == StatisticsShortTerm: - base_query = QUERY_STATISTICS_SHORT_TERM - else: - base_query = QUERY_STATISTICS + columns = [table.metadata_id, table.start] + if "last_reset" in types: + columns.append(table.last_reset) + if "max" in types: + columns.append(table.max) + if "mean" in types: + columns.append(table.mean) + if "min" in types: + columns.append(table.min) + if "state" in types: + columns.append(table.state) + if "sum" in types: + columns.append(table.sum) - query = session.query(*base_query) + stmt = lambda_stmt(lambda: select(columns)) most_recent_statistic_ids = ( - session.query( - func.max(table.id).label("max_id"), - ) + lambda_stmt(lambda: select(func.max(table.id).label("max_id"))) .filter(table.start < start_time) .filter(table.metadata_id.in_(metadata_ids)) + .group_by(table.metadata_id) + .subquery() ) - most_recent_statistic_ids = most_recent_statistic_ids.group_by(table.metadata_id) - most_recent_statistic_ids = most_recent_statistic_ids.subquery() - query = query.join( + + stmt += lambda q: q.join( most_recent_statistic_ids, table.id == most_recent_statistic_ids.c.max_id, ) - - return execute(query) + return execute_stmt_lambda_element(session, stmt) def _sorted_statistics_to_dict( @@ -1795,8 +1874,8 @@ def _sorted_statistics_to_dict( convert_units: bool, table: type[Statistics | StatisticsShortTerm], start_time: datetime | None, - start_time_as_datetime: bool, units: dict[str, str] | None, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> dict[str, list[dict]]: """Convert SQL results into JSON friendly data structure.""" result: dict = defaultdict(list) @@ -1822,7 +1901,9 @@ def _sorted_statistics_to_dict( # Fetch last known statistics for the needed metadata IDs if need_stat_at_start_time: assert start_time # Can not be None if need_stat_at_start_time is not empty - tmp = _statistics_at_time(session, need_stat_at_start_time, table, start_time) + tmp = _statistics_at_time( + session, need_stat_at_start_time, table, start_time, types + ) if tmp: for stat in tmp: stats_at_start_time[stat.metadata_id] = (stat,) @@ -1841,21 +1922,24 @@ def _sorted_statistics_to_dict( for db_state in chain(stats_at_start_time.get(meta_id, ()), group): start = process_timestamp(db_state.start) end = start + table.duration - ent_results.append( - { - "statistic_id": statistic_id, - "start": start if start_time_as_datetime else start.isoformat(), - "end": end.isoformat(), - "mean": convert(db_state.mean), - "min": convert(db_state.min), - "max": convert(db_state.max), - "last_reset": process_timestamp_to_utc_isoformat( - db_state.last_reset - ), - "state": convert(db_state.state), - "sum": convert(db_state.sum), - } - ) + row = { + "start": start, + "end": end, + } + if "mean" in types: + row["mean"] = convert(db_state.mean) + if "min" in types: + row["min"] = convert(db_state.min) + if "max" in types: + row["max"] = convert(db_state.max) + if "last_reset" in types: + row["last_reset"] = process_timestamp(db_state.last_reset) + if "state" in types: + row["state"] = convert(db_state.state) + if "sum" in types: + row["sum"] = convert(db_state.sum) + + ent_results.append(row) # Filter out the empty lists if some states had 0 results. return {metadata[key]["statistic_id"]: val for key, val in result.items() if val} @@ -1996,6 +2080,26 @@ def _filter_unique_constraint_integrity_error( return _filter_unique_constraint_integrity_error +def _import_statistics_with_session( + session: Session, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], + table: type[Statistics | StatisticsShortTerm], +) -> bool: + """Import statistics to the database.""" + old_metadata_dict = get_metadata_with_session( + session, statistic_ids=[metadata["statistic_id"]] + ) + metadata_id = _update_or_add_metadata(session, metadata, old_metadata_dict) + for stat in statistics: + if stat_id := _statistics_exists(session, table, metadata_id, stat["start"]): + _update_statistics(session, table, stat_id, stat) + else: + _insert_statistics(session, table, metadata_id, stat) + + return True + + @retryable_database_job("statistics") def import_statistics( instance: Recorder, @@ -2009,19 +2113,7 @@ def import_statistics( session=instance.get_session(), exception_filter=_filter_unique_constraint_integrity_error(instance), ) as session: - old_metadata_dict = get_metadata_with_session( - session, statistic_ids=[metadata["statistic_id"]] - ) - metadata_id = _update_or_add_metadata(session, metadata, old_metadata_dict) - for stat in statistics: - if stat_id := _statistics_exists( - session, table, metadata_id, stat["start"] - ): - _update_statistics(session, table, stat_id, stat) - else: - _insert_statistics(session, table, metadata_id, stat) - - return True + return _import_statistics_with_session(session, metadata, statistics, table) @retryable_database_job("adjust_statistics") @@ -2138,3 +2230,239 @@ def async_change_statistics_unit( new_unit_of_measurement=new_unit_of_measurement, old_unit_of_measurement=old_unit_of_measurement, ) + + +def _validate_db_schema_utf8( + instance: Recorder, session_maker: Callable[[], Session] +) -> set[str]: + """Do some basic checks for common schema errors caused by manual migration.""" + schema_errors: set[str] = set() + + # Lack of full utf8 support is only an issue for MySQL / MariaDB + if instance.dialect_name != SupportedDialect.MYSQL: + return schema_errors + + # This name can't be represented unless 4-byte UTF-8 unicode is supported + utf8_name = "𓆚𓃗" + statistic_id = f"{DOMAIN}.db_test" + + metadata: StatisticMetaData = { + "has_mean": True, + "has_sum": True, + "name": utf8_name, + "source": DOMAIN, + "statistic_id": statistic_id, + "unit_of_measurement": None, + } + + # Try inserting some metadata which needs utfmb4 support + try: + with session_scope(session=session_maker()) as session: + old_metadata_dict = get_metadata_with_session( + session, statistic_ids=[statistic_id] + ) + try: + _update_or_add_metadata(session, metadata, old_metadata_dict) + _clear_statistics_with_session(session, statistic_ids=[statistic_id]) + except OperationalError as err: + if err.orig and err.orig.args[0] == 1366: + _LOGGER.debug( + "Database table statistics_meta does not support 4-byte UTF-8" + ) + schema_errors.add("statistics_meta.4-byte UTF-8") + session.rollback() + else: + raise + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema: %s", exc) + return schema_errors + + +def _validate_db_schema( + hass: HomeAssistant, instance: Recorder, session_maker: Callable[[], Session] +) -> set[str]: + """Do some basic checks for common schema errors caused by manual migration.""" + schema_errors: set[str] = set() + + # Wrong precision is only an issue for MySQL / MariaDB / PostgreSQL + if instance.dialect_name not in ( + SupportedDialect.MYSQL, + SupportedDialect.POSTGRESQL, + ): + return schema_errors + + # This number can't be accurately represented as a 32-bit float + precise_number = 1.000000000000001 + # This time can't be accurately represented unless datetimes have µs precision + precise_time = datetime(2020, 10, 6, microsecond=1, tzinfo=dt_util.UTC) + + start_time = datetime(2020, 10, 6, tzinfo=dt_util.UTC) + statistic_id = f"{DOMAIN}.db_test" + + metadata: StatisticMetaData = { + "has_mean": True, + "has_sum": True, + "name": None, + "source": DOMAIN, + "statistic_id": statistic_id, + "unit_of_measurement": None, + } + statistics: StatisticData = { + "last_reset": precise_time, + "max": precise_number, + "mean": precise_number, + "min": precise_number, + "start": precise_time, + "state": precise_number, + "sum": precise_number, + } + + def check_columns( + schema_errors: set[str], + stored: Mapping, + expected: Mapping, + columns: tuple[str, ...], + table_name: str, + supports: str, + ) -> None: + for column in columns: + if stored[column] != expected[column]: + schema_errors.add(f"{table_name}.{supports}") + _LOGGER.debug( + "Column %s in database table %s does not support %s (%s != %s)", + column, + table_name, + supports, + stored[column], + expected[column], + ) + + # Insert / adjust a test statistics row in each of the tables + tables: tuple[type[Statistics | StatisticsShortTerm], ...] = ( + Statistics, + StatisticsShortTerm, + ) + try: + with session_scope(session=session_maker()) as session: + for table in tables: + _import_statistics_with_session(session, metadata, (statistics,), table) + stored_statistics = _statistics_during_period_with_session( + hass, + session, + start_time, + None, + [statistic_id], + "hour" if table == Statistics else "5minute", + None, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) + if not (stored_statistic := stored_statistics.get(statistic_id)): + _LOGGER.warning( + "Schema validation failed for table: %s", table.__tablename__ + ) + continue + + check_columns( + schema_errors, + stored_statistic[0], + statistics, + ("max", "mean", "min", "state", "sum"), + table.__tablename__, + "double precision", + ) + assert statistics["last_reset"] + check_columns( + schema_errors, + stored_statistic[0], + { + "last_reset": statistics["last_reset"], + "start": statistics["start"], + }, + ("start", "last_reset"), + table.__tablename__, + "µs precision", + ) + _clear_statistics_with_session(session, statistic_ids=[statistic_id]) + except Exception as exc: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema: %s", exc) + + return schema_errors + + +def validate_db_schema( + hass: HomeAssistant, instance: Recorder, session_maker: Callable[[], Session] +) -> set[str]: + """Do some basic checks for common schema errors caused by manual migration.""" + schema_errors: set[str] = set() + schema_errors |= _validate_db_schema_utf8(instance, session_maker) + schema_errors |= _validate_db_schema(hass, instance, session_maker) + if schema_errors: + _LOGGER.debug( + "Detected statistics schema errors: %s", ", ".join(sorted(schema_errors)) + ) + return schema_errors + + +def correct_db_schema( + instance: Recorder, + engine: Engine, + session_maker: Callable[[], Session], + schema_errors: set[str], +) -> None: + """Correct issues detected by validate_db_schema.""" + from .migration import _modify_columns # pylint: disable=import-outside-toplevel + + if "statistics_meta.4-byte UTF-8" in schema_errors: + # Attempt to convert the table to utf8mb4 + _LOGGER.warning( + "Updating character set and collation of table %s to utf8mb4. " + "Note: this can take several minutes on large databases and slow " + "computers. Please be patient!", + "statistics_meta", + ) + with contextlib.suppress(SQLAlchemyError): + with session_scope(session=session_maker()) as session: + connection = session.connection() + connection.execute( + # Using LOCK=EXCLUSIVE to prevent the database from corrupting + # https://github.com/home-assistant/core/issues/56104 + text( + "ALTER TABLE statistics_meta CONVERT TO " + "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, LOCK=EXCLUSIVE" + ) + ) + + tables: tuple[type[Statistics | StatisticsShortTerm], ...] = ( + Statistics, + StatisticsShortTerm, + ) + for table in tables: + if f"{table.__tablename__}.double precision" in schema_errors: + # Attempt to convert float columns to double precision + _modify_columns( + session_maker, + engine, + table.__tablename__, + [ + "mean DOUBLE PRECISION", + "min DOUBLE PRECISION", + "max DOUBLE PRECISION", + "state DOUBLE PRECISION", + "sum DOUBLE PRECISION", + ], + ) + if f"{table.__tablename__}.µs precision" in schema_errors: + # Attempt to convert datetime columns to µs precision + if instance.dialect_name == SupportedDialect.MYSQL: + datetime_type = "DATETIME(6)" + else: + datetime_type = "TIMESTAMP(6) WITH TIME ZONE" + _modify_columns( + session_maker, + engine, + table.__tablename__, + [ + f"last_reset {datetime_type}", + f"start {datetime_type}", + ], + ) diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 1b8e03ebf17..01723a50960 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -133,13 +133,14 @@ class StatisticsTask(RecorderTask): """An object to insert into the recorder queue to run a statistics task.""" start: datetime + fire_events: bool def run(self, instance: Recorder) -> None: """Run statistics task.""" - if statistics.compile_statistics(instance, self.start): + if statistics.compile_statistics(instance, self.start, self.fire_events): return # Schedule a new statistics task if this one didn't finish - instance.queue_task(StatisticsTask(self.start)) + instance.queue_task(StatisticsTask(self.start, self.fire_events)) @dataclass diff --git a/homeassistant/components/recorder/translations/sk.json b/homeassistant/components/recorder/translations/sk.json new file mode 100644 index 00000000000..69acec2ee46 --- /dev/null +++ b/homeassistant/components/recorder/translations/sk.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Aktu\u00e1lny \u010das spustenia", + "database_version": "Verzia datab\u00e1zy" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 8ee9a4e0401..a52a067b975 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -24,8 +24,10 @@ from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement from typing_extensions import Concatenate, ParamSpec +import voluptuous as vol from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv import homeassistant.util.dt as dt_util from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect @@ -35,7 +37,7 @@ from .db_schema import ( TABLES_TO_CHECK, RecorderRuns, ) -from .models import UnsupportedDialect, process_timestamp +from .models import StatisticPeriod, UnsupportedDialect, process_timestamp if TYPE_CHECKING: from . import Recorder @@ -604,3 +606,83 @@ def get_instance(hass: HomeAssistant) -> Recorder: """Get the recorder instance.""" instance: Recorder = hass.data[DATA_INSTANCE] return instance + + +PERIOD_SCHEMA = vol.Schema( + { + vol.Exclusive("calendar", "period"): vol.Schema( + { + vol.Required("period"): vol.Any("hour", "day", "week", "month", "year"), + vol.Optional("offset"): int, + } + ), + vol.Exclusive("fixed_period", "period"): vol.Schema( + { + vol.Optional("start_time"): vol.All(cv.datetime, dt_util.as_utc), + vol.Optional("end_time"): vol.All(cv.datetime, dt_util.as_utc), + } + ), + vol.Exclusive("rolling_window", "period"): vol.Schema( + { + vol.Required("duration"): cv.time_period_dict, + vol.Optional("offset"): cv.time_period_dict, + } + ), + } +) + + +def resolve_period( + period_def: StatisticPeriod, +) -> tuple[datetime | None, datetime | None]: + """Return start and end datetimes for a statistic period definition.""" + start_time = None + end_time = None + + if "calendar" in period_def: + calendar_period = period_def["calendar"]["period"] + start_of_day = dt_util.start_of_local_day() + cal_offset = period_def["calendar"].get("offset", 0) + if calendar_period == "hour": + start_time = dt_util.now().replace(minute=0, second=0, microsecond=0) + start_time += timedelta(hours=cal_offset) + end_time = start_time + timedelta(hours=1) + elif calendar_period == "day": + start_time = start_of_day + start_time += timedelta(days=cal_offset) + end_time = start_time + timedelta(days=1) + elif calendar_period == "week": + start_time = start_of_day - timedelta(days=start_of_day.weekday()) + start_time += timedelta(days=cal_offset * 7) + end_time = start_time + timedelta(weeks=1) + elif calendar_period == "month": + start_time = start_of_day.replace(day=28) + # This works for up to 48 months of offset + start_time = (start_time + timedelta(days=cal_offset * 31)).replace(day=1) + end_time = (start_time + timedelta(days=31)).replace(day=1) + else: # calendar_period = "year" + start_time = start_of_day.replace(month=12, day=31) + # This works for 100+ years of offset + start_time = (start_time + timedelta(days=cal_offset * 366)).replace( + month=1, day=1 + ) + end_time = (start_time + timedelta(days=365)).replace(day=1) + + start_time = dt_util.as_utc(start_time) + end_time = dt_util.as_utc(end_time) + + elif "fixed_period" in period_def: + start_time = period_def["fixed_period"].get("start_time") + end_time = period_def["fixed_period"].get("end_time") + + elif "rolling_window" in period_def: + duration = period_def["rolling_window"]["duration"] + now = dt_util.utcnow() + start_time = now - duration + end_time = start_time + duration + + if offset := period_def["rolling_window"].get("offset"): + start_time += offset + end_time += offset + + return (start_time, end_time) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 9b2ef417755..35879bfc076 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,9 +1,9 @@ """The Recorder websocket API.""" from __future__ import annotations -from datetime import datetime as dt, timedelta +from datetime import datetime as dt import logging -from typing import Any, Literal +from typing import Any, Literal, cast import voluptuous as vol @@ -26,6 +26,7 @@ from homeassistant.util.unit_conversion import ( ) from .const import MAX_QUEUE_BACKLOG +from .models import StatisticPeriod from .statistics import ( STATISTIC_UNIT_TO_UNIT_CONVERTER, async_add_external_statistics, @@ -36,7 +37,13 @@ from .statistics import ( statistics_during_period, validate_statistics, ) -from .util import async_migration_in_progress, async_migration_is_live, get_instance +from .util import ( + PERIOD_SCHEMA, + async_migration_in_progress, + async_migration_is_live, + get_instance, + resolve_period, +) _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -65,7 +72,7 @@ def _ws_get_statistic_during_period( start_time: dt | None, end_time: dt | None, statistic_id: str, - types: set[str] | None, + types: set[Literal["max", "mean", "min", "change"]] | None, units: dict[str, str], ) -> str: """Fetch statistics and convert them to json in the executor.""" @@ -82,26 +89,10 @@ def _ws_get_statistic_during_period( @websocket_api.websocket_command( { vol.Required("type"): "recorder/statistic_during_period", - vol.Exclusive("calendar", "period"): vol.Schema( - { - vol.Required("period"): vol.Any("hour", "day", "week", "month", "year"), - vol.Optional("offset"): int, - } - ), - vol.Exclusive("fixed_period", "period"): vol.Schema( - { - vol.Optional("start_time"): str, - vol.Optional("end_time"): str, - } - ), - vol.Exclusive("rolling_window", "period"): vol.Schema( - { - vol.Required("duration"): cv.time_period_dict, - vol.Optional("offset"): cv.time_period_dict, - } - ), vol.Optional("statistic_id"): str, - vol.Optional("types"): vol.All([str], vol.Coerce(set)), + vol.Optional("types"): vol.All( + [vol.Any("max", "mean", "min", "change")], vol.Coerce(set) + ), vol.Optional("units"): vol.Schema( { vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), @@ -114,6 +105,7 @@ def _ws_get_statistic_during_period( vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), } ), + **PERIOD_SCHEMA.schema, } ) @websocket_api.async_response @@ -126,67 +118,7 @@ async def ws_get_statistic_during_period( if "offset" in msg and "duration" not in msg: raise HomeAssistantError - start_time = None - end_time = None - - if "calendar" in msg: - calendar_period = msg["calendar"]["period"] - start_of_day = dt_util.start_of_local_day() - offset = msg["calendar"].get("offset", 0) - if calendar_period == "hour": - start_time = dt_util.now().replace(minute=0, second=0, microsecond=0) - start_time += timedelta(hours=offset) - end_time = start_time + timedelta(hours=1) - elif calendar_period == "day": - start_time = start_of_day - start_time += timedelta(days=offset) - end_time = start_time + timedelta(days=1) - elif calendar_period == "week": - start_time = start_of_day - timedelta(days=start_of_day.weekday()) - start_time += timedelta(days=offset * 7) - end_time = start_time + timedelta(weeks=1) - elif calendar_period == "month": - start_time = start_of_day.replace(day=28) - # This works for up to 48 months of offset - start_time = (start_time + timedelta(days=offset * 31)).replace(day=1) - end_time = (start_time + timedelta(days=31)).replace(day=1) - else: # calendar_period = "year" - start_time = start_of_day.replace(month=12, day=31) - # This works for 100+ years of offset - start_time = (start_time + timedelta(days=offset * 366)).replace( - month=1, day=1 - ) - end_time = (start_time + timedelta(days=365)).replace(day=1) - - start_time = dt_util.as_utc(start_time) - end_time = dt_util.as_utc(end_time) - - elif "fixed_period" in msg: - if start_time_str := msg["fixed_period"].get("start_time"): - if start_time := dt_util.parse_datetime(start_time_str): - start_time = dt_util.as_utc(start_time) - else: - connection.send_error( - msg["id"], "invalid_start_time", "Invalid start_time" - ) - return - - if end_time_str := msg["fixed_period"].get("end_time"): - if end_time := dt_util.parse_datetime(end_time_str): - end_time = dt_util.as_utc(end_time) - else: - connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") - return - - elif "rolling_window" in msg: - duration = msg["rolling_window"]["duration"] - now = dt_util.utcnow() - start_time = now - duration - end_time = start_time + duration - - if offset := msg["rolling_window"].get("offset"): - start_time += offset - end_time += offset + start_time, end_time = resolve_period(cast(StatisticPeriod, msg)) connection.send_message( await get_instance(hass).async_add_executor_job( @@ -210,16 +142,27 @@ def _ws_get_statistics_during_period( statistic_ids: list[str] | None, period: Literal["5minute", "day", "hour", "week", "month"], units: dict[str, str], + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], ) -> str: """Fetch statistics and convert them to json in the executor.""" - return JSON_DUMP( - messages.result_message( - msg_id, - statistics_during_period( - hass, start_time, end_time, statistic_ids, period, units=units - ), - ) + result = statistics_during_period( + hass, + start_time, + end_time, + statistic_ids, + period, + units, + types, ) + for statistic_id in result: + for item in result[statistic_id]: + if (start := item.get("start")) is not None: + item["start"] = int(start.timestamp() * 1000) + if (end := item.get("end")) is not None: + item["end"] = int(end.timestamp() * 1000) + if (last_reset := item.get("last_reset")) is not None: + item["last_reset"] = int(last_reset.timestamp() * 1000) + return JSON_DUMP(messages.result_message(msg_id, result)) async def ws_handle_get_statistics_during_period( @@ -244,6 +187,8 @@ async def ws_handle_get_statistics_during_period( else: end_time = None + if (types := msg.get("types")) is None: + types = {"last_reset", "max", "mean", "min", "state", "sum"} connection.send_message( await get_instance(hass).async_add_executor_job( _ws_get_statistics_during_period, @@ -254,6 +199,7 @@ async def ws_handle_get_statistics_during_period( msg.get("statistic_ids"), msg.get("period"), msg.get("units"), + types, ) ) @@ -277,6 +223,10 @@ async def ws_handle_get_statistics_during_period( vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), } ), + vol.Optional("types"): vol.All( + [vol.Any("last_reset", "max", "mean", "min", "state", "sum")], + vol.Coerce(set), + ), } ) @websocket_api.async_response diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 6ba5ca89d2d..17915e1be19 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass from datetime import timedelta -from enum import IntEnum +from enum import IntFlag import functools as ft import logging from typing import Any, final @@ -61,7 +61,7 @@ DEFAULT_DELAY_SECS = 0.4 DEFAULT_HOLD_SECS = 0 -class RemoteEntityFeature(IntEnum): +class RemoteEntityFeature(IntFlag): """Supported features of the remote entity.""" LEARN_COMMAND = 1 @@ -166,10 +166,10 @@ class RemoteEntity(ToggleEntity): entity_description: RemoteEntityDescription _attr_activity_list: list[str] | None = None _attr_current_activity: str | None = None - _attr_supported_features: int = 0 + _attr_supported_features: RemoteEntityFeature = RemoteEntityFeature(0) @property - def supported_features(self) -> int: + def supported_features(self) -> RemoteEntityFeature: """Flag supported features.""" return self._attr_supported_features diff --git a/homeassistant/components/remote/translations/is.json b/homeassistant/components/remote/translations/is.json index 3908be15d36..e36cffa6bee 100644 --- a/homeassistant/components/remote/translations/is.json +++ b/homeassistant/components/remote/translations/is.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} er sl\u00f6kkt", + "is_on": "{entity_name} er kveikt" + } + }, "state": { "_": { "off": "\u00d3virk", diff --git a/homeassistant/components/remote/translations/sk.json b/homeassistant/components/remote/translations/sk.json index 381668927df..4c8b090aea1 100644 --- a/homeassistant/components/remote/translations/sk.json +++ b/homeassistant/components/remote/translations/sk.json @@ -1,4 +1,9 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} zapnut\u00e9 alebo vypnut\u00e9" + } + }, "state": { "_": { "off": "Vypnut\u00fd", diff --git a/homeassistant/components/renault/device_tracker.py b/homeassistant/components/renault/device_tracker.py index da267277d10..87ca3c9eb5f 100644 --- a/homeassistant/components/renault/device_tracker.py +++ b/homeassistant/components/renault/device_tracker.py @@ -3,8 +3,7 @@ from __future__ import annotations from renault_api.kamereon.models import KamereonVehicleLocationData -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 5b2fc146e92..a9f65197502 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -1,6 +1,7 @@ { "domain": "renault", "name": "Renault", + "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/renault", "requirements": ["renault-api==0.1.11"], diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 3076dfc9f10..436b9209e89 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -23,7 +23,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, @@ -189,17 +188,22 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( + # For vehicles that DO NOT report charging power in watts, this seems to + # correspond to the maximum power that would be admissible by the car based + # on the battery state, regardless of the type of charger. key="charging_power", condition_lambda=lambda a: not a.details.reports_charging_power_in_watts(), coordinator="battery", data_key="chargingInstantaneousPower", - device_class=SensorDeviceClass.CURRENT, + device_class=SensorDeviceClass.POWER, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging power", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + name="Admissible charging power", + native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( + # For vehicles that DO report charging power in watts, this is the power + # effectively being transferred to the car. key="charging_power", condition_lambda=lambda a: a.details.reports_charging_power_in_watts(), coordinator="battery", diff --git a/homeassistant/components/renault/translations/bg.json b/homeassistant/components/renault/translations/bg.json index 364d397cb57..388b2dc6f8e 100644 --- a/homeassistant/components/renault/translations/bg.json +++ b/homeassistant/components/renault/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/renault/translations/sk.json b/homeassistant/components/renault/translations/sk.json index d1d6ad72898..79e529d998a 100644 --- a/homeassistant/components/renault/translations/sk.json +++ b/homeassistant/components/renault/translations/sk.json @@ -1,14 +1,23 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "invalid_credentials": "Neplatn\u00e9 overenie" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Aktualizujte svoje heslo pre {username}", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { + "password": "Heslo", "username": "Email" } } diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 9c26fe01a69..aa578c098d5 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -6,16 +6,27 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import ConfirmRepairFlow +from .issue_handler import ConfirmRepairFlow, RepairsFlowManager from .models import RepairsFlow __all__ = [ - "DOMAIN", "ConfirmRepairFlow", + "DOMAIN", + "repairs_flow_manager", "RepairsFlow", + "RepairsFlowManager", ] +def repairs_flow_manager(hass: HomeAssistant) -> RepairsFlowManager | None: + """Return the repairs flow manager.""" + if (domain_data := hass.data.get(DOMAIN)) is None: + return None + + flow_manager: RepairsFlowManager | None = domain_data.get("flow_manager") + return flow_manager + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Repairs.""" hass.data[DOMAIN] = {} diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 9c5e3854773..4914be8b520 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -85,7 +85,8 @@ class RepairsFlowManager(data_entry_flow.FlowManager): self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult ) -> data_entry_flow.FlowResult: """Complete a fix flow.""" - async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"]) + if result.get("type") != data_entry_flow.FlowResultType.ABORT: + async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"]) if "result" not in result: result["result"] = None return result diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 5549abc2143..4ec519dd709 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -2,9 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine import contextlib from datetime import timedelta import logging +from typing import Any import httpx import voluptuous as vol @@ -41,7 +43,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX from .data import RestData -from .schema import CONFIG_SCHEMA # noqa: F401 +from .schema import CONFIG_SCHEMA, RESOURCE_SCHEMA # noqa: F401 _LOGGER = logging.getLogger(__name__) @@ -88,15 +90,15 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool if DOMAIN not in config: return True - refresh_tasks = [] - load_tasks = [] + refresh_coroutines: list[Coroutine[Any, Any, None]] = [] + load_coroutines: list[Coroutine[Any, Any, None]] = [] rest_config: list[ConfigType] = config[DOMAIN] for rest_idx, conf in enumerate(rest_config): scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) resource_template: template.Template | None = conf.get(CONF_RESOURCE_TEMPLATE) rest = create_rest_data_from_config(hass, conf) coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval) - refresh_tasks.append(coordinator.async_refresh()) + refresh_coroutines.append(coordinator.async_refresh()) hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator}) for platform_domain in COORDINATOR_AWARE_PLATFORMS: @@ -107,20 +109,20 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool hass.data[DOMAIN][platform_domain].append(platform_conf) platform_idx = len(hass.data[DOMAIN][platform_domain]) - 1 - load = discovery.async_load_platform( + load_coroutine = discovery.async_load_platform( hass, platform_domain, DOMAIN, {REST_IDX: rest_idx, PLATFORM_IDX: platform_idx}, config, ) - load_tasks.append(load) + load_coroutines.append(load_coroutine) - if refresh_tasks: - await asyncio.gather(*refresh_tasks) + if refresh_coroutines: + await asyncio.gather(*refresh_coroutines) - if load_tasks: - await asyncio.gather(*load_tasks) + if load_coroutines: + await asyncio.gather(*load_coroutines) return True diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index bc51433c3c5..fc40d76a21d 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -100,24 +100,17 @@ class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): fallback_name=DEFAULT_BINARY_SENSOR_NAME, unique_id=unique_id, ) - self._state = False self._previous_data = None self._value_template = config.get(CONF_VALUE_TEMPLATE) if (value_template := self._value_template) is not None: value_template.hass = hass - self._is_on = None self._attr_device_class = config.get(CONF_DEVICE_CLASS) - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._is_on - def _update_from_rest_data(self): """Update state from the rest data.""" if self.rest.data is None: - self._is_on = False + self._attr_is_on = False response = self.rest.data @@ -127,8 +120,11 @@ class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): ) try: - self._is_on = bool(int(response)) + self._attr_is_on = bool(int(response)) except ValueError: - self._is_on = {"true": True, "on": True, "open": True, "yes": True}.get( - response.lower(), False - ) + self._attr_is_on = { + "true": True, + "on": True, + "open": True, + "yes": True, + }.get(response.lower(), False) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index f2a5d93cd22..cda35d1f918 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -81,8 +81,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the RESTful switch.""" - resource = config.get(CONF_RESOURCE) - unique_id = config.get(CONF_UNIQUE_ID) + resource: str = config[CONF_RESOURCE] + unique_id: str | None = config.get(CONF_UNIQUE_ID) try: switch = RestSwitch(hass, config, unique_id) @@ -106,10 +106,10 @@ class RestSwitch(TemplateEntity, SwitchEntity): def __init__( self, - hass, - config, - unique_id, - ): + hass: HomeAssistant, + config: ConfigType, + unique_id: str | None, + ) -> None: """Initialize the REST switch.""" TemplateEntity.__init__( self, @@ -119,41 +119,34 @@ class RestSwitch(TemplateEntity, SwitchEntity): unique_id=unique_id, ) - self._state = None - - auth = None + auth: aiohttp.BasicAuth | None = None + username: str | None = None if username := config.get(CONF_USERNAME): - auth = aiohttp.BasicAuth(username, password=config[CONF_PASSWORD]) + password: str = config[CONF_PASSWORD] + auth = aiohttp.BasicAuth(username, password=password) - self._resource = config.get(CONF_RESOURCE) - self._state_resource = config.get(CONF_STATE_RESOURCE) or self._resource - self._method = config.get(CONF_METHOD) - self._headers = config.get(CONF_HEADERS) - self._params = config.get(CONF_PARAMS) + self._resource: str = config[CONF_RESOURCE] + self._state_resource: str = config.get(CONF_STATE_RESOURCE) or self._resource + self._method: str = config[CONF_METHOD] + self._headers: dict[str, template.Template] | None = config.get(CONF_HEADERS) + self._params: dict[str, template.Template] | None = config.get(CONF_PARAMS) self._auth = auth - self._body_on = config.get(CONF_BODY_ON) - self._body_off = config.get(CONF_BODY_OFF) - self._is_on_template = config.get(CONF_IS_ON_TEMPLATE) - self._timeout = config.get(CONF_TIMEOUT) - self._verify_ssl = config.get(CONF_VERIFY_SSL) + self._body_on: template.Template = config[CONF_BODY_ON] + self._body_off: template.Template = config[CONF_BODY_OFF] + self._is_on_template: template.Template | None = config.get(CONF_IS_ON_TEMPLATE) + self._timeout: int = config[CONF_TIMEOUT] + self._verify_ssl: bool = config[CONF_VERIFY_SSL] self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._body_on.hass = hass + self._body_off.hass = hass if (is_on_template := self._is_on_template) is not None: is_on_template.hass = hass - if (body_on := self._body_on) is not None: - body_on.hass = hass - if (body_off := self._body_off) is not None: - body_off.hass = hass template.attach(hass, self._headers) template.attach(hass, self._params) - @property - def is_on(self): - """Return true if device is on.""" - return self._state - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" body_on_t = self._body_on.async_render(parse_result=False) @@ -162,7 +155,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): req = await self.set_device_state(body_on_t) if req.status == HTTPStatus.OK: - self._state = True + self._attr_is_on = True else: _LOGGER.error( "Can't turn on %s. Is resource/endpoint offline?", self._resource @@ -177,7 +170,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): try: req = await self.set_device_state(body_off_t) if req.status == HTTPStatus.OK: - self._state = False + self._attr_is_on = False else: _LOGGER.error( "Can't turn off %s. Is resource/endpoint offline?", self._resource @@ -185,7 +178,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while switching off %s", self._resource) - async def set_device_state(self, body): + async def set_device_state(self, body: Any) -> aiohttp.ClientResponse: """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) @@ -193,7 +186,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): - req = await getattr(websession, self._method)( + req: aiohttp.ClientResponse = await getattr(websession, self._method)( self._resource, auth=self._auth, data=bytes(body, "utf-8"), @@ -211,7 +204,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): except aiohttp.ClientError as err: _LOGGER.exception("Error while fetching data: %s", err) - async def get_device_state(self, hass): + async def get_device_state(self, hass: HomeAssistant) -> aiohttp.ClientResponse: """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) @@ -233,17 +226,17 @@ class RestSwitch(TemplateEntity, SwitchEntity): ) text = text.lower() if text == "true": - self._state = True + self._attr_is_on = True elif text == "false": - self._state = False + self._attr_is_on = False else: - self._state = None + self._attr_is_on = None else: if text == self._body_on.template: - self._state = True + self._attr_is_on = True elif text == self._body_off.template: - self._state = False + self._attr_is_on = False else: - self._state = None + self._attr_is_on = None return req diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 70f887cf896..c26f9711e1f 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -154,7 +154,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # register services hass.services.async_register(DOMAIN, name, async_service_handler) - for command, command_config in config[DOMAIN].items(): - async_register_rest_command(command, command_config) + for name, command_config in config[DOMAIN].items(): + async_register_rest_command(name, command_config) return True diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 7bc87de9f46..cfa76a57e67 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -337,6 +337,7 @@ class RflinkDevice(Entity): # Rflink specific attributes for every component type self._initial_event = initial_event self._device_id = device_id + self._attr_unique_id = device_id if name: self._name = name else: diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 2420e933653..baccd47ebb3 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -1,16 +1,37 @@ """Support for Rflink sensors.""" from __future__ import annotations +from typing import Any + from rflink.parser import PACKET_FIELDS, UNITS import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_PARTS_PER_MILLION, CONF_DEVICES, CONF_NAME, CONF_SENSOR_TYPE, CONF_UNIT_OF_MEASUREMENT, + DEGREE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + LIGHT_LUX, + PERCENTAGE, + PRECIPITATION_MILLIMETERS, + UV_INDEX, + UnitOfLength, + UnitOfPower, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -32,11 +53,213 @@ from . import ( RflinkDevice, ) -SENSOR_ICONS = { - "humidity": "mdi:water-percent", - "battery": "mdi:battery", - "temperature": "mdi:thermometer", -} +SENSOR_TYPES = ( + # check new descriptors against PACKET_FIELDS & UNITS from rflink.parser + SensorEntityDescription( + key="average_windspeed", + name="Average windspeed", + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + ), + SensorEntityDescription( + key="barometric_pressure", + name="Barometric pressure", + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.HPA, + ), + SensorEntityDescription( + key="battery", + name="Battery", + icon="mdi:battery", + ), + SensorEntityDescription( + key="co2_air_quality", + name="CO2 air quality", + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + ), + SensorEntityDescription( + key="command", + name="Command", + icon="mdi:text", + ), + SensorEntityDescription( + key="current_phase_1", + name="Current phase 1", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + SensorEntityDescription( + key="current_phase_2", + name="Current phase 2", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + SensorEntityDescription( + key="current_phase_3", + name="Current phase 3", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + SensorEntityDescription( + key="distance", + name="Distance", + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + ), + SensorEntityDescription( + key="doorbell_melody", + name="Doorbell melody", + icon="mdi:bell", + ), + SensorEntityDescription( + key="firmware", + name="Firmware", + icon="mdi:information-outline", + ), + SensorEntityDescription( + key="hardware", + name="Hardware", + icon="mdi:chip", + ), + SensorEntityDescription( + key="humidity", + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="humidity_status", + name="Humidity status", + icon="mdi:water-percent", + ), + SensorEntityDescription( + key="kilowatt", + name="Kilowatt", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + ), + SensorEntityDescription( + key="light_intensity", + name="Light intensity", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + SensorEntityDescription( + key="meter_value", + name="Meter value", + icon="mdi:counter", + ), + SensorEntityDescription( + key="noise_level", + name="Noise level", + icon="mdi:bell-alert", + ), + SensorEntityDescription( + key="rain_rate", + name="Rain rate", + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + ), + SensorEntityDescription( + key="revision", + name="Revision", + icon="mdi:information", + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + SensorEntityDescription( + key="total_rain", + name="Total rain", + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=PRECIPITATION_MILLIMETERS, + ), + SensorEntityDescription( + key="uv_intensity", + name="UV intensity", + icon="mdi:sunglasses", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UV_INDEX, + ), + SensorEntityDescription( + key="version", + name="Version", + icon="mdi:information", + ), + SensorEntityDescription( + key="voltage", + name="Voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + SensorEntityDescription( + key="watt", + name="Watt", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + ), + SensorEntityDescription( + key="weather_forecast", + name="Weather forecast", + icon="mdi:weather-cloudy-clock", + ), + SensorEntityDescription( + key="windchill", + name="Wind chill", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + SensorEntityDescription( + key="winddirection", + name="Wind direction", + icon="mdi:compass", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=DEGREE, + ), + SensorEntityDescription( + key="windgusts", + name="Wind gusts", + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + ), + SensorEntityDescription( + key="windspeed", + name="Wind speed", + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + ), + SensorEntityDescription( + key="windtemp", + name="Wind temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), +) + +SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -72,10 +295,6 @@ def devices_from_config(domain_config): """Parse configuration and add Rflink sensor devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): - if ATTR_UNIT_OF_MEASUREMENT not in config: - config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type( - config[CONF_SENSOR_TYPE] - ) device = RflinkSensor(device_id, **config) devices.append(device) @@ -112,11 +331,21 @@ class RflinkSensor(RflinkDevice, SensorEntity): """Representation of a Rflink sensor.""" def __init__( - self, device_id, sensor_type, unit_of_measurement, initial_event=None, **kwargs - ): + self, + device_id: str, + sensor_type: str, + unit_of_measurement: str | None = None, + initial_event=None, + **kwargs: Any, + ) -> None: """Handle sensor specific args and super init.""" self._sensor_type = sensor_type self._unit_of_measurement = unit_of_measurement + if sensor_type in SENSOR_TYPES_DICT: + self.entity_description = SENSOR_TYPES_DICT[sensor_type] + elif not unit_of_measurement: + self._unit_of_measurement = lookup_unit_for_sensor_type(sensor_type) + super().__init__(device_id, initial_event=initial_event, **kwargs) def _handle_event(self, event): @@ -164,15 +393,13 @@ class RflinkSensor(RflinkDevice, SensorEntity): @property def native_unit_of_measurement(self): """Return measurement unit.""" - return self._unit_of_measurement + if self._unit_of_measurement: + return self._unit_of_measurement + if hasattr(self, "entity_description"): + return self.entity_description.native_unit_of_measurement + return None @property def native_value(self): """Return value.""" return self._state - - @property - def icon(self): - """Return possible sensor specific icon.""" - if self._sensor_type in SENSOR_ICONS: - return SENSOR_ICONS[self._sensor_type] diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index 445a93b7e76..2661fc41211 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -68,7 +68,7 @@ "set_device_options": { "data": { "command_off": "Datenbitwert f\u00fcr den Befehl \"aus\"", - "command_on": "Datenbitwert f\u00fcr den Befehl \"ein\"", + "command_on": "Datenbitwert f\u00fcr den Befehl \"an\"", "data_bit": "Anzahl der Datenbits", "off_delay": "Ausschaltverz\u00f6gerung", "off_delay_enabled": "Ausschaltverz\u00f6gerung aktivieren", diff --git a/homeassistant/components/rfxtrx/translations/sk.json b/homeassistant/components/rfxtrx/translations/sk.json index e343d2e8b31..83867537e65 100644 --- a/homeassistant/components/rfxtrx/translations/sk.json +++ b/homeassistant/components/rfxtrx/translations/sk.json @@ -1,9 +1,60 @@ { "config": { + "abort": { + "already_configured": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "setup_network": { "data": { + "host": "Hostite\u013e", "port": "Port" + }, + "title": "Vyberte adresu pripojenia" + }, + "setup_serial": { + "data": { + "device": "Vyberte zariadenie" + }, + "title": "Zariadenie" + }, + "setup_serial_manual_path": { + "data": { + "device": "Cesta k zariadeniu USB" + }, + "title": "Cesta" + }, + "user": { + "data": { + "type": "Typ pripojenia" + }, + "title": "Vyberte typ pripojenia" + } + } + }, + "device_automation": { + "action_type": { + "send_command": "Odosla\u0165 pr\u00edkaz: {subtype}", + "send_status": "Odosla\u0165 aktualiz\u00e1ciu stavu: {subtype}" + }, + "trigger_type": { + "command": "Prijat\u00fd pr\u00edkaz: {subtype}", + "status": "Prijat\u00fd stav: {subtype}" + } + }, + "options": { + "error": { + "already_configured_device": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "prompt_options": { + "data": { + "device": "Vyberte zariadenie, ktor\u00e9 chcete nakonfigurova\u0165", + "protocols": "Protokoly" } } } diff --git a/homeassistant/components/rhasspy/translations/de.json b/homeassistant/components/rhasspy/translations/de.json index 953cb89400a..00b09289479 100644 --- a/homeassistant/components/rhasspy/translations/de.json +++ b/homeassistant/components/rhasspy/translations/de.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "M\u00f6chtest du die Rhasspy-Unterst\u00fctzung aktivieren?" + "description": "M\u00f6chtest du die Rhasspy Unterst\u00fctzung aktivieren?" } } } diff --git a/homeassistant/components/rhasspy/translations/sk.json b/homeassistant/components/rhasspy/translations/sk.json new file mode 100644 index 00000000000..c294bc45d7c --- /dev/null +++ b/homeassistant/components/rhasspy/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/bg.json b/homeassistant/components/ridwell/translations/bg.json index a0418dd4af0..fd738789a07 100644 --- a/homeassistant/components/ridwell/translations/bg.json +++ b/homeassistant/components/ridwell/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/ridwell/translations/sk.json b/homeassistant/components/ridwell/translations/sk.json index 71a7aea5018..d74f548d29a 100644 --- a/homeassistant/components/ridwell/translations/sk.json +++ b/homeassistant/components/ridwell/translations/sk.json @@ -1,10 +1,28 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Znova zadajte heslo pre {username}:", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte pou\u017e\u00edvate\u013esk\u00e9 meno a heslo:" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 1aaa073064f..10736c7b85c 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -206,7 +206,7 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( name="Battery", category=["doorbots", "authorized_doorbots", "stickup_cams"], native_unit_of_measurement=PERCENTAGE, - device_class="battery", + device_class=SensorDeviceClass.BATTERY, cls=RingSensor, ), RingSensorEntityDescription( @@ -255,7 +255,7 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, icon="mdi:wifi", - device_class="signal_strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, cls=HealthDataRingSensor, ), ) diff --git a/homeassistant/components/ring/translations/bg.json b/homeassistant/components/ring/translations/bg.json index dfe9fcc384e..1b9e9ad53d6 100644 --- a/homeassistant/components/ring/translations/bg.json +++ b/homeassistant/components/ring/translations/bg.json @@ -12,7 +12,7 @@ "data": { "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" }, - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/ring/translations/sk.json b/homeassistant/components/ring/translations/sk.json index 5ada995aa6e..805e6449829 100644 --- a/homeassistant/components/ring/translations/sk.json +++ b/homeassistant/components/ring/translations/sk.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "2fa": { + "data": { + "2fa": "Dvojfaktorov\u00fd k\u00f3d" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 0e631cc4a93..f143244d31d 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -26,7 +26,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store @@ -40,7 +40,12 @@ from .const import ( TYPE_LOCAL, ) -PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SWITCH, +] LAST_EVENT_STORAGE_VERSION = 1 LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp" _LOGGER = logging.getLogger(__name__) @@ -127,10 +132,9 @@ async def _async_setup_cloud_entry(hass: HomeAssistant, entry: ConfigEntry) -> b try: await risco.login(async_get_clientsession(hass)) except CannotConnectError as error: - raise ConfigEntryNotReady() from error - except UnauthorizedError: - _LOGGER.exception("Failed to login to Risco cloud") - return False + raise ConfigEntryNotReady from error + except UnauthorizedError as error: + raise ConfigEntryAuthFailed from error scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) coordinator = RiscoDataUpdateCoordinator(hass, risco, scan_interval) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 79da100d6e1..116e022d216 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -40,7 +40,7 @@ from .const import ( RISCO_GROUPS, RISCO_PARTIAL_ARM, ) -from .entity import RiscoEntity +from .entity import RiscoCloudEntity _LOGGER = logging.getLogger(__name__) @@ -107,7 +107,6 @@ class RiscoAlarm(AlarmControlPanelEntity): self._code_disarm_required = options[CONF_CODE_DISARM_REQUIRED] self._risco_to_ha = options[CONF_RISCO_STATES_TO_HA] self._ha_to_risco = options[CONF_HA_STATES_TO_RISCO] - self._attr_supported_features = 0 self._attr_has_entity_name = True self._attr_name = None for state in self._ha_to_risco: @@ -178,7 +177,7 @@ class RiscoAlarm(AlarmControlPanelEntity): raise NotImplementedError -class RiscoCloudAlarm(RiscoAlarm, RiscoEntity): +class RiscoCloudAlarm(RiscoAlarm, RiscoCloudEntity): """Representation of a Risco partition.""" def __init__( diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index bc021c2c364..423137d88b6 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -12,21 +12,11 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LocalData, RiscoDataUpdateCoordinator, is_local, zone_update_signal +from . import LocalData, RiscoDataUpdateCoordinator, is_local from .const import DATA_COORDINATOR, DOMAIN -from .entity import RiscoEntity, binary_sensor_unique_id - -SERVICE_BYPASS_ZONE = "bypass_zone" -SERVICE_UNBYPASS_ZONE = "unbypass_zone" - - -def _unique_id_for_local(system_id: str, zone_id: int) -> str: - return f"{system_id}_zone_{zone_id}_local" +from .entity import RiscoCloudZoneEntity, RiscoLocalZoneEntity async def async_setup_entry( @@ -35,21 +25,16 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Risco alarm control panel.""" - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_BYPASS_ZONE, {}, "async_bypass_zone") - platform.async_register_entity_service( - SERVICE_UNBYPASS_ZONE, {}, "async_unbypass_zone" - ) - if is_local(config_entry): local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - RiscoLocalBinarySensor(local_data.system.id, zone_id, zone) - for zone_id, zone in local_data.system.zones.items() - ) - async_add_entities( - RiscoLocalAlarmedBinarySensor(local_data.system.id, zone_id, zone) + entity for zone_id, zone in local_data.system.zones.items() + for entity in ( + RiscoLocalBinarySensor(local_data.system.id, zone_id, zone), + RiscoLocalAlarmedBinarySensor(local_data.system.id, zone_id, zone), + RiscoLocalArmedBinarySensor(local_data.system.id, zone_id, zone), + ) ) else: coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][ @@ -61,85 +46,34 @@ async def async_setup_entry( ) -class RiscoBinarySensor(BinarySensorEntity): - """Representation of a Risco zone as a binary sensor.""" +class RiscoCloudBinarySensor(RiscoCloudZoneEntity, BinarySensorEntity): + """Representation of a Risco cloud zone as a binary sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION - def __init__(self, *, zone_id: int, zone: Zone, **kwargs: Any) -> None: + def __init__( + self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone + ) -> None: """Init the zone.""" - super().__init__(**kwargs) - self._zone_id = zone_id - self._zone = zone - self._attr_has_entity_name = True - self._attr_name = None - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return the state attributes.""" - return {"zone_id": self._zone_id, "bypassed": self._zone.bypassed} + super().__init__( + coordinator=coordinator, name=None, suffix="", zone_id=zone_id, zone=zone + ) @property def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._zone.triggered - async def async_bypass_zone(self) -> None: - """Bypass this zone.""" - await self._bypass(True) - async def async_unbypass_zone(self) -> None: - """Unbypass this zone.""" - await self._bypass(False) - - async def _bypass(self, bypass: bool) -> None: - raise NotImplementedError - - -class RiscoCloudBinarySensor(RiscoBinarySensor, RiscoEntity): - """Representation of a Risco cloud zone as a binary sensor.""" - - def __init__( - self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone - ) -> None: - """Init the zone.""" - super().__init__(zone_id=zone_id, zone=zone, coordinator=coordinator) - self._attr_unique_id = binary_sensor_unique_id(self._risco, zone_id) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer="Risco", - name=self._zone.name, - ) - - def _get_data_from_coordinator(self) -> None: - self._zone = self.coordinator.data.zones[self._zone_id] - - async def _bypass(self, bypass: bool) -> None: - alarm = await self._risco.bypass_zone(self._zone_id, bypass) - self._zone = alarm.zones[self._zone_id] - self.async_write_ha_state() - - -class RiscoLocalBinarySensor(RiscoBinarySensor): +class RiscoLocalBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation of a Risco local zone as a binary sensor.""" - _attr_should_poll = False + _attr_device_class = BinarySensorDeviceClass.MOTION def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" - super().__init__(zone_id=zone_id, zone=zone) - self._attr_unique_id = _unique_id_for_local(system_id, zone_id) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer="Risco", - name=self._zone.name, - ) - - async def async_added_to_hass(self) -> None: - """Subscribe to updates.""" - signal = zone_update_signal(self._zone_id) - self.async_on_remove( - async_dispatcher_connect(self.hass, signal, self.async_write_ha_state) + super().__init__( + system_id=system_id, name=None, suffix="", zone_id=zone_id, zone=zone ) @property @@ -150,43 +84,45 @@ class RiscoLocalBinarySensor(RiscoBinarySensor): "groups": self._zone.groups, } - async def _bypass(self, bypass: bool) -> None: - await self._zone.bypass(bypass) + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return self._zone.triggered -class RiscoLocalAlarmedBinarySensor(BinarySensorEntity): +class RiscoLocalAlarmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation whether a zone in Risco local is currently triggering an alarm.""" - _attr_should_poll = False - def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" - super().__init__() - self._zone_id = zone_id - self._zone = zone - self._attr_has_entity_name = True - self._attr_name = "Alarmed" - device_unique_id = _unique_id_for_local(system_id, zone_id) - self._attr_unique_id = device_unique_id + "_alarmed" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_unique_id)}, - manufacturer="Risco", - name=self._zone.name, + super().__init__( + system_id=system_id, + name="Alarmed", + suffix="_alarmed", + zone_id=zone_id, + zone=zone, ) - async def async_added_to_hass(self) -> None: - """Subscribe to updates.""" - signal = zone_update_signal(self._zone_id) - self.async_on_remove( - async_dispatcher_connect(self.hass, signal, self.async_write_ha_state) - ) - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return the state attributes.""" - return {"zone_id": self._zone_id} - @property def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._zone.alarmed + + +class RiscoLocalArmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): + """Representation whether a zone in Risco local is currently armed.""" + + def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: + """Init the zone.""" + super().__init__( + system_id=system_id, + name="Armed", + suffix="_armed", + zone_id=zone_id, + zone=zone, + ) + + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return self._zone.armed diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 5e1cdb75b5a..0f532a376a1 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging +from typing import Any from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError import voluptuous as vol @@ -21,6 +22,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -93,6 +95,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Init the config flow.""" + self._reauth_entry: config_entries.ConfigEntry | None = None + @staticmethod @core.callback def async_get_options_flow( @@ -112,8 +118,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Configure a cloud based alarm.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() + if not self._reauth_entry: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() try: info = await validate_cloud_input(self.hass, user_input) @@ -125,12 +132,25 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry(title=info["title"], data=user_input) + if not self._reauth_entry: + return self.async_create_entry(title=info["title"], data=user_input) + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data=user_input, + unique_id=user_input[CONF_USERNAME], + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") return self.async_show_form( step_id="cloud", data_schema=CLOUD_SCHEMA, errors=errors ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauth_entry = await self.async_set_unique_id(entry_data[CONF_USERNAME]) + return await self.async_step_cloud() + async def async_step_local(self, user_input=None): """Configure a local based alarm.""" errors = {} @@ -138,6 +158,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_local_input(self.hass, user_input) except CannotConnectError: + _LOGGER.debug("Cannot connect", exc_info=1) errors["base"] = "cannot_connect" except UnauthorizedError: errors["base"] = "invalid_auth" diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py index e49b632ac78..a4ac260887c 100644 --- a/homeassistant/components/risco/entity.py +++ b/homeassistant/components/risco/entity.py @@ -1,25 +1,40 @@ """A risco entity base class.""" +from __future__ import annotations + +from typing import Any + +from pyrisco.common import Zone + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import RiscoDataUpdateCoordinator +from . import RiscoDataUpdateCoordinator, zone_update_signal +from .const import DOMAIN -def binary_sensor_unique_id(risco, zone_id: int) -> str: - """Return unique id for the binary sensor.""" +def zone_unique_id(risco, zone_id: int) -> str: + """Return unique id for a cloud zone.""" return f"{risco.site_uuid}_zone_{zone_id}" -class RiscoEntity(CoordinatorEntity[RiscoDataUpdateCoordinator]): - """Risco entity base class.""" +class RiscoCloudEntity(CoordinatorEntity[RiscoDataUpdateCoordinator]): + """Risco cloud entity base class.""" - def _get_data_from_coordinator(self): + def __init__( + self, *, coordinator: RiscoDataUpdateCoordinator, **kwargs: Any + ) -> None: + """Init the entity.""" + super().__init__(coordinator=coordinator, **kwargs) + + def _get_data_from_coordinator(self) -> None: raise NotImplementedError - def _refresh_from_coordinator(self): + def _refresh_from_coordinator(self) -> None: self._get_data_from_coordinator() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( self.coordinator.async_add_listener(self._refresh_from_coordinator) @@ -29,3 +44,74 @@ class RiscoEntity(CoordinatorEntity[RiscoDataUpdateCoordinator]): def _risco(self): """Return the Risco API object.""" return self.coordinator.risco + + +class RiscoCloudZoneEntity(RiscoCloudEntity): + """Risco cloud zone entity base class.""" + + _attr_has_entity_name = True + + def __init__( + self, + *, + coordinator: RiscoDataUpdateCoordinator, + name: str | None, + suffix: str, + zone_id: int, + zone: Zone, + **kwargs: Any, + ) -> None: + """Init the zone.""" + super().__init__(coordinator=coordinator, **kwargs) + self._zone_id = zone_id + self._zone = zone + self._attr_name = name + device_unique_id = zone_unique_id(self._risco, zone_id) + self._attr_unique_id = f"{device_unique_id}{suffix}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_unique_id)}, + manufacturer="Risco", + name=self._zone.name, + ) + self._attr_extra_state_attributes = {"zone_id": zone_id} + + def _get_data_from_coordinator(self) -> None: + self._zone = self.coordinator.data.zones[self._zone_id] + + +class RiscoLocalZoneEntity(Entity): + """Risco local zone entity base class.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + *, + system_id: str, + name: str | None, + suffix: str, + zone_id: int, + zone: Zone, + **kwargs: Any, + ) -> None: + """Init the zone.""" + super().__init__(**kwargs) + self._zone_id = zone_id + self._zone = zone + self._attr_name = name + device_unique_id = f"{system_id}_zone_{zone_id}_local" + self._attr_unique_id = f"{device_unique_id}{suffix}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_unique_id)}, + manufacturer="Risco", + name=zone.name, + ) + self._attr_extra_state_attributes = {"zone_id": zone_id} + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + signal = zone_update_signal(self._zone_id) + self.async_on_remove( + async_dispatcher_connect(self.hass, signal, self.async_write_ha_state) + ) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index bd8bbfd715f..d31d148f4da 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.5"], + "requirements": ["pyrisco==0.5.6"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index c4bd047e260..f2cb9821166 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -15,7 +15,7 @@ from homeassistant.util import dt as dt_util from . import RiscoEventsDataUpdateCoordinator, is_local from .const import DOMAIN, EVENTS_COORDINATOR -from .entity import binary_sensor_unique_id +from .entity import zone_unique_id CATEGORIES = { 2: "Alarm", @@ -115,11 +115,9 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): attrs = {atr: getattr(self._event, atr, None) for atr in EVENT_ATTRIBUTES} if self._event.zone_id is not None: - zone_unique_id = binary_sensor_unique_id( - self.coordinator.risco, self._event.zone_id - ) + uid = zone_unique_id(self.coordinator.risco, self._event.zone_id) zone_entity_id = self._entity_registry.async_get_entity_id( - BS_DOMAIN, DOMAIN, zone_unique_id + BS_DOMAIN, DOMAIN, uid ) if zone_entity_id is not None: attrs["zone_entity_id"] = zone_entity_id diff --git a/homeassistant/components/risco/services.yaml b/homeassistant/components/risco/services.yaml deleted file mode 100644 index c271df7b462..00000000000 --- a/homeassistant/components/risco/services.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Describes the format for available Risco services - -bypass_zone: - name: Bypass zone - description: Bypass a Risco Zone - target: - entity: - integration: risco - domain: binary_sensor - -unbypass_zone: - name: Unbypass zone - description: Unbypass a Risco Zone - target: - entity: - integration: risco - domain: binary_sensor diff --git a/homeassistant/components/risco/switch.py b/homeassistant/components/risco/switch.py new file mode 100644 index 00000000000..2ed07b9f34b --- /dev/null +++ b/homeassistant/components/risco/switch.py @@ -0,0 +1,104 @@ +"""Support for bypassing Risco alarm zones.""" +from __future__ import annotations + +from pyrisco.common import Zone + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import LocalData, RiscoDataUpdateCoordinator, is_local +from .const import DATA_COORDINATOR, DOMAIN +from .entity import RiscoCloudZoneEntity, RiscoLocalZoneEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Risco switch.""" + if is_local(config_entry): + local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + RiscoLocalSwitch(local_data.system.id, zone_id, zone) + for zone_id, zone in local_data.system.zones.items() + ) + else: + coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ][DATA_COORDINATOR] + async_add_entities( + RiscoCloudSwitch(coordinator, zone_id, zone) + for zone_id, zone in coordinator.data.zones.items() + ) + + +class RiscoCloudSwitch(RiscoCloudZoneEntity, SwitchEntity): + """Representation of a bypass switch for a Risco cloud zone.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone + ) -> None: + """Init the zone.""" + super().__init__( + coordinator=coordinator, + name="Bypassed", + suffix="_bypassed", + zone_id=zone_id, + zone=zone, + ) + + @property + def is_on(self) -> bool | None: + """Return true if the zone is bypassed.""" + return self._zone.bypassed + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + await self._bypass(True) + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + await self._bypass(False) + + async def _bypass(self, bypass: bool) -> None: + alarm = await self._risco.bypass_zone(self._zone_id, bypass) + self._zone = alarm.zones[self._zone_id] + self.async_write_ha_state() + + +class RiscoLocalSwitch(RiscoLocalZoneEntity, SwitchEntity): + """Representation of a bypass switch for a Risco local zone.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: + """Init the zone.""" + super().__init__( + system_id=system_id, + name="Bypassed", + suffix="_bypassed", + zone_id=zone_id, + zone=zone, + ) + + @property + def is_on(self) -> bool | None: + """Return true if the zone is bypassed.""" + return self._zone.bypassed + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + await self._bypass(True) + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + await self._bypass(False) + + async def _bypass(self, bypass: bool) -> None: + await self._zone.bypass(bypass) diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index 5e165a9dcfe..a738c47d6f2 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - }, "menu_options": { "cloud": "Risco Cloud (\u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u043e)", "local": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d \u043f\u0430\u043d\u0435\u043b Risco (\u0437\u0430 \u043d\u0430\u043f\u0440\u0435\u0434\u043d\u0430\u043b\u0438)" diff --git a/homeassistant/components/risco/translations/ca.json b/homeassistant/components/risco/translations/ca.json index 072f9521543..7f3cbf0d52c 100644 --- a/homeassistant/components/risco/translations/ca.json +++ b/homeassistant/components/risco/translations/ca.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Contrasenya", - "pin": "Codi PIN", - "username": "Nom d'usuari" - }, "menu_options": { "cloud": "Risco Cloud (recomanat)", "local": "Panell Risco local (avan\u00e7at)" diff --git a/homeassistant/components/risco/translations/cs.json b/homeassistant/components/risco/translations/cs.json index 8698052c0fa..0e2bd5924b0 100644 --- a/homeassistant/components/risco/translations/cs.json +++ b/homeassistant/components/risco/translations/cs.json @@ -22,13 +22,6 @@ "pin": "PIN k\u00f3d", "port": "Port" } - }, - "user": { - "data": { - "password": "Heslo", - "pin": "PIN k\u00f3d", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } } } }, diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index 6b356a473ce..3699b0ecd8f 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -24,14 +24,9 @@ } }, "user": { - "data": { - "password": "Passwort", - "pin": "PIN-Code", - "username": "Benutzername" - }, "menu_options": { - "cloud": "Risco-Cloud (empfohlen)", - "local": "Lokales Risco-Panel (fortgeschritten)" + "cloud": "Risco Cloud (empfohlen)", + "local": "Lokales Risco Panel (fortgeschritten)" } } } diff --git a/homeassistant/components/risco/translations/el.json b/homeassistant/components/risco/translations/el.json index 4d68d564184..9b4424600c7 100644 --- a/homeassistant/components/risco/translations/el.json +++ b/homeassistant/components/risco/translations/el.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, "menu_options": { "cloud": "Risco Cloud (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)", "local": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03cc \u03a0\u03ac\u03bd\u03b5\u03bb Risco (\u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2)" diff --git a/homeassistant/components/risco/translations/en.json b/homeassistant/components/risco/translations/en.json index 0d72ba3cca2..95dd395e501 100644 --- a/homeassistant/components/risco/translations/en.json +++ b/homeassistant/components/risco/translations/en.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Password", - "pin": "PIN Code", - "username": "Username" - }, "menu_options": { "cloud": "Risco Cloud (recommended)", "local": "Local Risco Panel (advanced)" diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index 690887b66d2..ee6a998da07 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Contrase\u00f1a", - "pin": "C\u00f3digo PIN", - "username": "Nombre de usuario" - }, "menu_options": { "cloud": "Risco Cloud (recomendado)", "local": "Panel Risco local (avanzado)" diff --git a/homeassistant/components/risco/translations/et.json b/homeassistant/components/risco/translations/et.json index 4f8140c12fc..d28ebdfd167 100644 --- a/homeassistant/components/risco/translations/et.json +++ b/homeassistant/components/risco/translations/et.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Salas\u00f5na", - "pin": "PIN kood", - "username": "Kasutajanimi" - }, "menu_options": { "cloud": "Risco Cloud (soovitatav)", "local": "Kohalik Risco paneel (t\u00e4psem)" diff --git a/homeassistant/components/risco/translations/fr.json b/homeassistant/components/risco/translations/fr.json index 3d12d0be5c8..ff7edbbda4e 100644 --- a/homeassistant/components/risco/translations/fr.json +++ b/homeassistant/components/risco/translations/fr.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Mot de passe", - "pin": "Code PIN", - "username": "Nom d'utilisateur" - }, "menu_options": { "cloud": "Risco Cloud (recommand\u00e9)", "local": "Risco Panel local (avanc\u00e9)" diff --git a/homeassistant/components/risco/translations/he.json b/homeassistant/components/risco/translations/he.json index 926afdf8abf..fac0ad1a0b1 100644 --- a/homeassistant/components/risco/translations/he.json +++ b/homeassistant/components/risco/translations/he.json @@ -22,13 +22,6 @@ "pin": "\u05e7\u05d5\u05d3 PIN", "port": "\u05e4\u05ea\u05d7\u05d4" } - }, - "user": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "pin": "\u05e7\u05d5\u05d3 PIN", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } } } }, diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json index 0de2158f626..20b7daa3687 100644 --- a/homeassistant/components/risco/translations/hu.json +++ b/homeassistant/components/risco/translations/hu.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Jelsz\u00f3", - "pin": "PIN-k\u00f3d", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, "menu_options": { "cloud": "Risco Cloud (aj\u00e1nlott)", "local": "Helyi Risco panel (halad\u00f3)" diff --git a/homeassistant/components/risco/translations/id.json b/homeassistant/components/risco/translations/id.json index 438ff72a4ed..7fcdca45d38 100644 --- a/homeassistant/components/risco/translations/id.json +++ b/homeassistant/components/risco/translations/id.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Kata Sandi", - "pin": "Kode PIN", - "username": "Nama Pengguna" - }, "menu_options": { "cloud": "Risco Cloud (disarankan)", "local": "Panel Risco Lokal (lanjutan)" diff --git a/homeassistant/components/risco/translations/it.json b/homeassistant/components/risco/translations/it.json index ebe78f338d9..df714cbd9fc 100644 --- a/homeassistant/components/risco/translations/it.json +++ b/homeassistant/components/risco/translations/it.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Password", - "pin": "Codice PIN", - "username": "Nome utente" - }, "menu_options": { "cloud": "Risco Cloud (consigliato)", "local": "Pannello Risco locale (avanzato)" diff --git a/homeassistant/components/risco/translations/ja.json b/homeassistant/components/risco/translations/ja.json index a2919b167e5..f2243bbbe6a 100644 --- a/homeassistant/components/risco/translations/ja.json +++ b/homeassistant/components/risco/translations/ja.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "pin": "PIN\u30b3\u30fc\u30c9", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - }, "menu_options": { "cloud": "Risco Cloud(\u63a8\u5968)", "local": "Local Risco Panel(\u30a2\u30c9\u30d0\u30f3\u30b9\u30c9)" diff --git a/homeassistant/components/risco/translations/ko.json b/homeassistant/components/risco/translations/ko.json index 6ceaedbc6dc..f9cd9a28cc4 100644 --- a/homeassistant/components/risco/translations/ko.json +++ b/homeassistant/components/risco/translations/ko.json @@ -7,15 +7,6 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "pin": "PIN \ucf54\ub4dc", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } - } } }, "options": { diff --git a/homeassistant/components/risco/translations/lb.json b/homeassistant/components/risco/translations/lb.json index ae136cb1843..1cab6cbfa32 100644 --- a/homeassistant/components/risco/translations/lb.json +++ b/homeassistant/components/risco/translations/lb.json @@ -7,15 +7,6 @@ "cannot_connect": "Feeler beim verbannen", "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "password": "Passwuert", - "pin": "PIN Code", - "username": "Benotzernumm" - } - } } }, "options": { diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index db2a4996c77..e16f43d1767 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -22,13 +22,6 @@ "pin": "Pincode", "port": "Poort" } - }, - "user": { - "data": { - "password": "Wachtwoord", - "pin": "Pincode", - "username": "Gebruikersnaam" - } } } }, diff --git a/homeassistant/components/risco/translations/no.json b/homeassistant/components/risco/translations/no.json index 177a092f4f3..03c75284d53 100644 --- a/homeassistant/components/risco/translations/no.json +++ b/homeassistant/components/risco/translations/no.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Passord", - "pin": "PIN kode", - "username": "Brukernavn" - }, "menu_options": { "cloud": "Risco Cloud (anbefalt)", "local": "Lokalt Risco-panel (avansert)" diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index 0e4ea5302d7..f14d10a4bd6 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Has\u0142o", - "pin": "Kod PIN", - "username": "Nazwa u\u017cytkownika" - }, "menu_options": { "cloud": "Chmura Risco (zalecane)", "local": "Lokalny Panel Risco (zaawansowane)" diff --git a/homeassistant/components/risco/translations/pt-BR.json b/homeassistant/components/risco/translations/pt-BR.json index 7e3a9ef6818..14e07f4c09d 100644 --- a/homeassistant/components/risco/translations/pt-BR.json +++ b/homeassistant/components/risco/translations/pt-BR.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Senha", - "pin": "C\u00f3digo PIN", - "username": "Usu\u00e1rio" - }, "menu_options": { "cloud": "Risco Cloud (recomendado)", "local": "Painel de Risco Local (avan\u00e7ado)" diff --git a/homeassistant/components/risco/translations/pt.json b/homeassistant/components/risco/translations/pt.json index 30b87b0a0da..bdbab687bc0 100644 --- a/homeassistant/components/risco/translations/pt.json +++ b/homeassistant/components/risco/translations/pt.json @@ -10,11 +10,6 @@ }, "step": { "user": { - "data": { - "password": "Palavra-passe", - "pin": "C\u00f3digo PIN", - "username": "Nome de Utilizador" - }, "menu_options": { "cloud": "Risco Cloud (recomendado)", "local": "Painel de Risco Local (avan\u00e7ado)" diff --git a/homeassistant/components/risco/translations/ru.json b/homeassistant/components/risco/translations/ru.json index 116168a60de..d1f5748f81a 100644 --- a/homeassistant/components/risco/translations/ru.json +++ b/homeassistant/components/risco/translations/ru.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "pin": "PIN-\u043a\u043e\u0434", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, "menu_options": { "cloud": "\u041e\u0431\u043b\u0430\u043a\u043e Risco (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)", "local": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u0430\u043d\u0435\u043b\u044c Risco (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f)" diff --git a/homeassistant/components/risco/translations/sk.json b/homeassistant/components/risco/translations/sk.json index 3f464b4046d..b7c83c9709e 100644 --- a/homeassistant/components/risco/translations/sk.json +++ b/homeassistant/components/risco/translations/sk.json @@ -1,14 +1,40 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "cloud": { + "data": { + "password": "Heslo", + "pin": "PIN k\u00f3d", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "local": { + "data": { + "host": "Hostite\u013e", + "pin": "PIN k\u00f3d", + "port": "Port" + } + } } }, "options": { "step": { + "init": { + "title": "Konfigur\u00e1cia mo\u017enost\u00ed" + }, "risco_to_ha": { "data": { - "A": "Skupina A" + "A": "Skupina A", + "B": "Skupina B", + "C": "Skupina C", + "D": "Skupina D" } } } diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index dd89d9c43f7..862a9a3b1ba 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "L\u00f6senord", - "pin": "Pin-kod", - "username": "Anv\u00e4ndarnamn" - }, "menu_options": { "cloud": "Risco Cloud (rekommenderas)", "local": "Lokal Risco Panel (avancerat)" diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json index 6572e50de4f..83b7bc5c5f9 100644 --- a/homeassistant/components/risco/translations/tr.json +++ b/homeassistant/components/risco/translations/tr.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "Parola", - "pin": "PIN Kodu", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, "menu_options": { "cloud": "Risco Cloud (\u00f6nerilir)", "local": "Yerel Risco Paneli (geli\u015fmi\u015f)" diff --git a/homeassistant/components/risco/translations/uk.json b/homeassistant/components/risco/translations/uk.json index 53b64344f2e..794fcfdbcda 100644 --- a/homeassistant/components/risco/translations/uk.json +++ b/homeassistant/components/risco/translations/uk.json @@ -7,15 +7,6 @@ "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": { diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index 852ba76e208..a235abffc52 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -24,11 +24,6 @@ } }, "user": { - "data": { - "password": "\u5bc6\u78bc", - "pin": "PIN \u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, "menu_options": { "cloud": "Risco Cloud\uff08\u63a8\u85a6\uff09", "local": "\u672c\u5730\u7aef Risco \u9762\u677f\uff08\u9032\u968e\uff09" diff --git a/homeassistant/components/rituals_perfume_genie/translations/bg.json b/homeassistant/components/rituals_perfume_genie/translations/bg.json index 7be659cab0b..51b337ce813 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/bg.json +++ b/homeassistant/components/rituals_perfume_genie/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/sk.json b/homeassistant/components/rituals_perfume_genie/translations/sk.json index 72b0304f1c3..ab95d8060d4 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/sk.json +++ b/homeassistant/components/rituals_perfume_genie/translations/sk.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" } } } diff --git a/homeassistant/components/roku/translations/sk.json b/homeassistant/components/roku/translations/sk.json index bee0999420f..80f5e0b1cc7 100644 --- a/homeassistant/components/roku/translations/sk.json +++ b/homeassistant/components/roku/translations/sk.json @@ -1,7 +1,24 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Zadajte svoje inform\u00e1cie o Roku." + } } } } \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/sk.json b/homeassistant/components/roomba/translations/sk.json new file mode 100644 index 00000000000..946fbadf22a --- /dev/null +++ b/homeassistant/components/roomba/translations/sk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "not_irobot_device": "Zisten\u00e9 zariadenie nie je zariadenie iRobot" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name} ({host})", + "step": { + "link": { + "title": "Op\u00e4tovne z\u00edska\u0165 heslo" + }, + "link_manual": { + "data": { + "password": "Heslo" + }, + "title": "Zadajte heslo" + }, + "manual": { + "data": { + "host": "Hostite\u013e" + }, + "title": "Manu\u00e1lne pripojenie k zariadeniu" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "title": "Automaticky sa pripoji\u0165 k zariadeniu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index c80f92834bb..227a7e5c6a1 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -98,24 +98,19 @@ class RoonDevice(MediaPlayerEntity): """Initialize Roon device object.""" self._remove_signal_status = None self._server = server - self._available = True - self._last_position_update = None + self._attr_available = True self._supports_standby = False - self._state = MediaPlayerState.IDLE - self._unique_id = None + self._attr_state = MediaPlayerState.IDLE self._zone_id = None self._output_id = None - self._name = DEVICE_DEFAULT_NAME - self._media_title = None - self._media_album_name = None - self._media_artist = None - self._media_position = 0 - self._media_duration = 0 - self._is_volume_muted = False - self._volume_step = 0 - self._shuffle = False - self._media_image_url = None - self._volume_level = 0 + self._attr_name = DEVICE_DEFAULT_NAME + self._attr_media_position = 0 + self._attr_media_duration = 0 + self._attr_is_volume_muted = False + self._attr_volume_step = 0 + self._attr_shuffle = False + self._attr_media_image_url = None + self._attr_volume_level = 0 self.update_data(player_data) async def async_added_to_hass(self) -> None: @@ -135,11 +130,6 @@ class RoonDevice(MediaPlayerEntity): self.update_data(player_data) self.async_write_ha_state() - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def group_members(self): """Return the grouped players.""" @@ -148,8 +138,10 @@ class RoonDevice(MediaPlayerEntity): return [self._server.entity_id(roon_name) for roon_name in roon_names] @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return the device info.""" + if self.unique_id is None: + return None if self.player_data.get("source_controls"): dev_model = self.player_data["source_controls"][0].get("display_name") return DeviceInfo( @@ -166,14 +158,14 @@ class RoonDevice(MediaPlayerEntity): self.player_data = player_data if not self.player_data["is_available"]: # this player was removed - self._available = False - self._state = MediaPlayerState.OFF + self._attr_available = False + self._attr_state = MediaPlayerState.OFF else: - self._available = True + self._attr_available = True # determine player state self.update_state() if self.state == MediaPlayerState.PLAYING: - self._last_position_update = utcnow() + self._attr_media_position_updated_at = utcnow() @classmethod def _parse_volume(cls, player_data): @@ -263,37 +255,25 @@ class RoonDevice(MediaPlayerEntity): new_state = MediaPlayerState.PAUSED else: new_state = MediaPlayerState.IDLE - self._state = new_state - self._unique_id = self.player_data["dev_id"] + self._attr_state = new_state + self._attr_unique_id = self.player_data["dev_id"] self._zone_id = self.player_data["zone_id"] self._output_id = self.player_data["output_id"] - self._shuffle = self.player_data["settings"]["shuffle"] - self._name = self.player_data["display_name"] + self._attr_shuffle = self.player_data["settings"]["shuffle"] + self._attr_name = self.player_data["display_name"] volume = RoonDevice._parse_volume(self.player_data) - self._is_volume_muted = volume["muted"] - self._volume_step = volume["step"] - self._is_volume_muted = volume["muted"] - self._volume_level = volume["level"] + self._attr_is_volume_muted = volume["muted"] + self._attr_volume_step = volume["step"] + self._attr_volume_level = volume["level"] now_playing = self._parse_now_playing(self.player_data) - self._media_title = now_playing["title"] - self._media_artist = now_playing["artist"] - self._media_album_name = now_playing["album"] - self._media_position = now_playing["position"] - self._media_duration = now_playing["duration"] - self._media_image_url = now_playing["image"] - - @property - def media_position_updated_at(self): - """When was the position of the current playing media valid.""" - # Returns value from homeassistant.util.dt.utcnow(). - return self._last_position_update - - @property - def unique_id(self): - """Return the id of this roon client.""" - return self._unique_id + self._attr_media_title = now_playing["title"] + self._attr_media_artist = now_playing["artist"] + self._attr_media_album_name = now_playing["album"] + self._attr_media_position = now_playing["position"] + self._attr_media_duration = now_playing["duration"] + self._attr_media_image_url = now_playing["image"] @property def zone_id(self): @@ -306,75 +286,15 @@ class RoonDevice(MediaPlayerEntity): return self._output_id @property - def name(self): - """Return device name.""" - return self._name - - @property - def media_title(self): - """Return title currently playing.""" - return self._media_title - - @property - def media_album_name(self): - """Album name of current playing media (Music track only).""" - return self._media_album_name - - @property - def media_artist(self): - """Artist of current playing media (Music track only).""" - return self._media_artist - - @property - def media_album_artist(self): + def media_album_artist(self) -> str | None: """Album artist of current playing media (Music track only).""" - return self._media_artist - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._media_image_url - - @property - def media_position(self): - """Return position currently playing.""" - return self._media_position - - @property - def media_duration(self): - """Return total runtime length.""" - return self._media_duration - - @property - def volume_level(self): - """Return current volume level.""" - return self._volume_level - - @property - def is_volume_muted(self): - """Return mute state.""" - return self._is_volume_muted - - @property - def volume_step(self): - """.Return volume step size.""" - return self._volume_step + return self.media_artist @property def supports_standby(self): """Return power state of source controls.""" return self._supports_standby - @property - def state(self): - """Return current playstate of the device.""" - return self._state - - @property - def shuffle(self): - """Boolean if shuffle is enabled.""" - return self._shuffle - def media_play(self) -> None: """Send play command to device.""" self._server.roonapi.playback_control(self.output_id, "play") @@ -403,7 +323,7 @@ class RoonDevice(MediaPlayerEntity): """Send seek command to device.""" self._server.roonapi.seek(self.output_id, position) # Seek doesn't cause an async update - so force one - self._media_position = position + self._attr_media_position = round(position) self.schedule_update_ha_state() def set_volume_level(self, volume: float) -> None: diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 8d7c53a28ee..5b839262424 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Port" }, - "description": "Der Roon-Server konnte nicht gefunden werden, bitte gib deinen Hostnamen und Port ein." + "description": "Der Roon Server konnte nicht gefunden werden, bitte gib deinen Hostnamen und Port ein." }, "link": { "description": "Du musst den Home Assistant in Roon autorisieren. Nachdem du auf \"Senden\" gedr\u00fcckt hast, gehe zur Roon Core-Anwendung, \u00f6ffne die Einstellungen und aktiviere HomeAssistant auf der Registerkarte \"Extensions\".", diff --git a/homeassistant/components/roon/translations/sk.json b/homeassistant/components/roon/translations/sk.json index 5ada995aa6e..2d0aa74c391 100644 --- a/homeassistant/components/roon/translations/sk.json +++ b/homeassistant/components/roon/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "fallback": { + "data": { + "host": "Hostite\u013e", + "port": "Port" + }, + "description": "Server Roon sa nepodarilo objavi\u0165, zadajte n\u00e1zov hostite\u013ea a port." + } } } } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/sk.json b/homeassistant/components/rpi_power/translations/sk.json index d19ecb22698..a0f761dd5dc 100644 --- a/homeassistant/components/rpi_power/translations/sk.json +++ b/homeassistant/components/rpi_power/translations/sk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Nie je mo\u017en\u00e9 n\u00e1js\u0165 syst\u00e9mov\u00fa triedu pre t\u00fato komponentu, uistite sa \u017ee v\u00e1\u0161 kernel je aktu\u00e1lny a hardv\u00e9r je podporovan\u00fd" + "no_devices_found": "Nie je mo\u017en\u00e9 n\u00e1js\u0165 syst\u00e9mov\u00fa triedu pre t\u00fato komponentu, uistite sa \u017ee v\u00e1\u0161 kernel je aktu\u00e1lny a hardv\u00e9r je podporovan\u00fd", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." }, "step": { "confirm": { diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index 865a6bafcb6..f66a0a30d8c 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -73,7 +73,7 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return None async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: - """Prepare confiugration for the RTSPtoWebRTC server add-on discovery.""" + """Prepare configuration for the RTSPtoWebRTC server add-on discovery.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/rtsp_to_webrtc/translations/cs.json b/homeassistant/components/rtsp_to_webrtc/translations/cs.json index 19f5d1e1587..7a90e894e42 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/cs.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/cs.json @@ -3,5 +3,14 @@ "abort": { "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Adresa serveru Stun (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/de.json b/homeassistant/components/rtsp_to_webrtc/translations/de.json index 75c7d590aa8..256e3574cbd 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/de.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/de.json @@ -1,25 +1,25 @@ { "config": { "abort": { - "server_failure": "Der RTSPtoWebRTC-Server hat einen Fehler gemeldet. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen.", - "server_unreachable": "Die Kommunikation mit dem RTSPtoWebRTC-Server ist nicht m\u00f6glich. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen.", + "server_failure": "Der RTSPtoWebRTC Server hat einen Fehler gemeldet. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen.", + "server_unreachable": "Die Kommunikation mit dem RTSPtoWebRTC Server ist nicht m\u00f6glich. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "invalid_url": "Muss eine g\u00fcltige RTSPtoWebRTC-Server-URL sein, z. B. https://example.com", - "server_failure": "Der RTSPtoWebRTC-Server hat einen Fehler gemeldet. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen.", - "server_unreachable": "Die Kommunikation mit dem RTSPtoWebRTC-Server ist nicht m\u00f6glich. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen." + "invalid_url": "Muss eine g\u00fcltige RTSPtoWebRTC Server-URL sein, z. B. https://example.com", + "server_failure": "Der RTSPtoWebRTC Server hat einen Fehler gemeldet. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen.", + "server_unreachable": "Die Kommunikation mit dem RTSPtoWebRTC Server ist nicht m\u00f6glich. Pr\u00fcfe die Protokolle f\u00fcr weitere Informationen." }, "step": { "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er sich mit dem RTSPtoWebRTC-Server verbindet, der vom Add-on bereitgestellt wird? {addon}?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er sich mit dem RTSPtoWebRTC Server verbindet, der vom Add-on bereitgestellt wird? {addon}?", "title": "RTSPtoWebRTC \u00fcber das Home Assistant-Add-on" }, "user": { "data": { "server_url": "RTSPtoWebRTC Server URL z.B. https://example.com" }, - "description": "Die RTSPtoWebRTC-Integration erfordert einen Server, der RTSP-Streams in WebRTC \u00fcbersetzt. Gib die URL f\u00fcr den RTSPtoWebRTC-Server ein.", + "description": "Die RTSPtoWebRTC Integration erfordert einen Server, der RTSP Streams in WebRTC \u00fcbersetzt. Gib die URL f\u00fcr den RTSPtoWebRTC Server ein.", "title": "RTSPtoWebRTC konfigurieren" } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/sk.json b/homeassistant/components/rtsp_to_webrtc/translations/sk.json new file mode 100644 index 00000000000..acecadcff42 --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_url": "Mus\u00ed to by\u0165 platn\u00e1 adresa URL servera RTSPtoWebRTC, napr. https://example.com" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index c9d9df6068e..5e8998c47dd 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -1,8 +1,7 @@ """Support for Ruckus Unleashed devices.""" from __future__ import annotations -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry diff --git a/homeassistant/components/ruckus_unleashed/translations/sk.json b/homeassistant/components/ruckus_unleashed/translations/sk.json index dbe0480911b..3ae0f994a0c 100644 --- a/homeassistant/components/ruckus_unleashed/translations/sk.json +++ b/homeassistant/components/ruckus_unleashed/translations/sk.json @@ -1,11 +1,18 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { + "host": "Hostite\u013e", + "password": "Heslo", "username": "U\u017e\u00edvate\u013esk\u00e9 meno" } } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index c639e5ddc90..fc0dca341a8 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -126,13 +126,14 @@ class RussoundZoneDevice(MediaPlayerEntity): return self._zone_var("name", self._name) @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" status = self._zone_var("status", "OFF") if status == "ON": return MediaPlayerState.ON if status == "OFF": return MediaPlayerState.OFF + return None @property def source(self): diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index 6782d783a83..7a384656b66 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -82,15 +82,11 @@ class RussoundRNETDevice(MediaPlayerEntity): def __init__(self, hass, russ, sources, zone_id, extra): """Initialise the Russound RNET device.""" - self._name = extra["name"] + self._attr_name = extra["name"] self._russ = russ - self._sources = sources + self._attr_source_list = sources self._zone_id = zone_id - self._state = None - self._volume = None - self._source = None - def update(self) -> None: """Retrieve latest state.""" # Updated this function to make a single call to get_zone_info, so that @@ -101,47 +97,21 @@ class RussoundRNETDevice(MediaPlayerEntity): if ret is not None: _LOGGER.debug("Updating status for zone %s", self._zone_id) if ret[0] == 0: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF else: - self._state = MediaPlayerState.ON - self._volume = ret[2] * 2 / 100.0 + self._attr_state = MediaPlayerState.ON + self._attr_volume_level = ret[2] * 2 / 100.0 # Returns 0 based index for source. index = ret[1] # Possibility exists that user has defined list of all sources. # If a source is set externally that is beyond the defined list then # an exception will be thrown. # In this case return and unknown source (None) - try: - self._source = self._sources[index] - except IndexError: - self._source = None + if self.source_list and 0 <= index < len(self.source_list): + self._attr_source = self.source_list[index] else: _LOGGER.error("Could not update status for zone %s", self._zone_id) - @property - def name(self): - """Return the name of the zone.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def source(self): - """Get the currently selected source.""" - return self._source - - @property - def volume_level(self): - """Volume level of the media player (0..1). - - Value is returned based on a range (0..100). - Therefore float divide by 100 to get to the required range. - """ - return self._volume - def set_volume_level(self, volume: float) -> None: """Set volume level. Volume has a range (0..1). @@ -164,12 +134,7 @@ class RussoundRNETDevice(MediaPlayerEntity): def select_source(self, source: str) -> None: """Set the input source.""" - if source in self._sources: - index = self._sources.index(source) + if self.source_list and source in self.source_list: + index = self.source_list.index(source) # 0 based value for source self._russ.set_source("1", self._zone_id, index) - - @property - def source_list(self): - """Return a list of available input sources.""" - return self._sources diff --git a/homeassistant/components/ruuvitag_ble/__init__.py b/homeassistant/components/ruuvitag_ble/__init__.py new file mode 100644 index 00000000000..5e30820f837 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/__init__.py @@ -0,0 +1,49 @@ +"""The ruuvitag_ble integration.""" +from __future__ import annotations + +import logging + +from ruuvitag_ble import RuuvitagBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ruuvitag BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = RuuvitagBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ruuvitag_ble/config_flow.py b/homeassistant/components/ruuvitag_ble/config_flow.py new file mode 100644 index 00000000000..620b901f4fe --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for ruuvitag_ble.""" +from __future__ import annotations + +from typing import Any + +from ruuvitag_ble import RuuvitagBluetoothDeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class RuuvitagConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for ruuvitag_ble.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: RuuvitagBluetoothDeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = RuuvitagBluetoothDeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = RuuvitagBluetoothDeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/ruuvitag_ble/const.py b/homeassistant/components/ruuvitag_ble/const.py new file mode 100644 index 00000000000..0df74a24eea --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the ruuvitag_ble integration.""" + +DOMAIN = "ruuvitag_ble" diff --git a/homeassistant/components/ruuvitag_ble/manifest.json b/homeassistant/components/ruuvitag_ble/manifest.json new file mode 100644 index 00000000000..a3500fca7c6 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "ruuvitag_ble", + "name": "RuuviTag BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ruuvitag_ble", + "bluetooth": [ + { + "manufacturer_id": 1177 + }, + { + "local_name": "Ruuvi *" + } + ], + "requirements": ["ruuvitag-ble==0.1.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@akx"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/ruuvitag_ble/sensor.py b/homeassistant/components/ruuvitag_ble/sensor.py new file mode 100644 index 00000000000..f99e78f65a6 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/sensor.py @@ -0,0 +1,150 @@ +"""Support for RuuviTag sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from sensor_state_data import ( + DeviceKey, + SensorDescription, + SensorDeviceClass as SSDSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries, const +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (SSDSensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=const.TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (SSDSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=const.PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (SSDSensorDeviceClass.PRESSURE, Units.PRESSURE_HPA): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.PRESSURE}_{Units.PRESSURE_HPA}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=const.PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + SSDSensorDeviceClass.VOLTAGE, + Units.ELECTRIC_POTENTIAL_MILLIVOLT, + ): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_MILLIVOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=const.ELECTRIC_POTENTIAL_MILLIVOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + SSDSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=const.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + (SSDSensorDeviceClass.COUNT, None): SensorEntityDescription( + key="movement_counter", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _to_sensor_key( + description: SensorDescription, +) -> tuple[SSDSensorDeviceClass, Units | None]: + assert description.device_class is not None + return (description.device_class, description.native_unit_of_measurement) + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass_device_info(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + _to_sensor_key(description) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if _to_sensor_key(description) in SENSOR_DESCRIPTIONS + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Ruuvitag BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + RuuvitagBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class RuuvitagBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Ruuvitag BLE sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sabnzbd/translations/sk.json b/homeassistant/components/sabnzbd/translations/sk.json index 9d5ee388dc3..d87df07af6d 100644 --- a/homeassistant/components/sabnzbd/translations/sk.json +++ b/homeassistant/components/sabnzbd/translations/sk.json @@ -1,9 +1,16 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" + "api_key": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov", + "path": "Cesta", + "url": "URL" } } } diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index b7988a558bd..8a922fbbaf0 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -1,13 +1,14 @@ { "domain": "samsungtv", "name": "Samsung Smart TV", + "integration_type": "device", "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.32.2" + "async-upnp-client==0.32.3" ], "ssdp": [ { diff --git a/homeassistant/components/samsungtv/translations/bg.json b/homeassistant/components/samsungtv/translations/bg.json index 56d85b8ff5d..f9fa03680a4 100644 --- a/homeassistant/components/samsungtv/translations/bg.json +++ b/homeassistant/components/samsungtv/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/samsungtv/translations/sk.json b/homeassistant/components/samsungtv/translations/sk.json index d4a3e2e9fbb..4be02aa40f4 100644 --- a/homeassistant/components/samsungtv/translations/sk.json +++ b/homeassistant/components/samsungtv/translations/sk.json @@ -1,12 +1,28 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "id_missing": "Toto zariadenie Samsung nem\u00e1 s\u00e9riov\u00e9 \u010d\u00edslo.", + "not_supported": "Toto zariadenie Samsung moment\u00e1lne nie je podporovan\u00e9.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "error": { + "invalid_pin": "PIN je neplatn\u00fd, sk\u00faste to znova." + }, + "flow_title": "{device}", "step": { + "encrypted_pairing": { + "description": "Zadajte k\u00f3d PIN zobrazen\u00fd na {device}." + }, + "reauth_confirm_encrypted": { + "description": "Zadajte k\u00f3d PIN zobrazen\u00fd na {device}." + }, "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov" } } diff --git a/homeassistant/components/schedule/translations/sk.json b/homeassistant/components/schedule/translations/sk.json new file mode 100644 index 00000000000..06788442e27 --- /dev/null +++ b/homeassistant/components/schedule/translations/sk.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "off": "Neakt\u00edvny", + "on": "Akt\u00edvny" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index f9222c126b5..cb5657a9649 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1 +1,116 @@ """The scrape component.""" +from __future__ import annotations + +import asyncio +from collections.abc import Coroutine +from datetime import timedelta +from typing import Any + +import voluptuous as vol + +from homeassistant.components.rest import RESOURCE_SCHEMA, create_rest_data_from_config +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ATTRIBUTE, + CONF_SCAN_INTERVAL, + CONF_VALUE_TEMPLATE, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_INDEX, CONF_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS +from .coordinator import ScrapeCoordinator + +SENSOR_SCHEMA = vol.Schema( + { + **TEMPLATE_SENSOR_BASE_SCHEMA.schema, + vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_INDEX, default=0): cv.positive_int, + vol.Required(CONF_SELECT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) + +COMBINED_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, + **RESOURCE_SCHEMA, + vol.Optional(SENSOR_DOMAIN): vol.All( + cv.ensure_list, [vol.Schema(SENSOR_SCHEMA)] + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [COMBINED_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Scrape from yaml config.""" + scrape_config: list[ConfigType] | None + if not (scrape_config := config.get(DOMAIN)): + return True + + load_coroutines: list[Coroutine[Any, Any, None]] = [] + for resource_config in scrape_config: + rest = create_rest_data_from_config(hass, resource_config) + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + coordinator = ScrapeCoordinator(hass, rest, scan_interval) + + sensors: list[ConfigType] = resource_config.get(SENSOR_DOMAIN, []) + if sensors: + load_coroutines.append( + discovery.async_load_platform( + hass, + Platform.SENSOR, + DOMAIN, + {"coordinator": coordinator, "configs": sensors}, + config, + ) + ) + + if load_coroutines: + await asyncio.gather(*load_coroutines) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Scrape from a config entry.""" + + rest_config: dict[str, Any] = COMBINED_SCHEMA(dict(entry.options)) + rest = create_rest_data_from_config(hass, rest_config) + + coordinator = ScrapeCoordinator( + hass, + rest, + DEFAULT_SCAN_INTERVAL, + ) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Scrape config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index a32e371a487..cbd0ed7d525 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -3,11 +3,16 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any +import uuid import voluptuous as vol +from homeassistant.components.rest import create_rest_data_from_config +from homeassistant.components.rest.data import DEFAULT_TIMEOUT +from homeassistant.components.rest.schema import DEFAULT_METHOD, METHODS from homeassistant.components.sensor import ( CONF_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorStateClass, ) @@ -16,21 +21,28 @@ from homeassistant.const import ( CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_HEADERS, + CONF_METHOD, CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, + CONF_TIMEOUT, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, + UnitOfTemperature, ) +from homeassistant.core import async_get_hass +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, SchemaConfigFlowHandler, + SchemaFlowError, SchemaFlowFormStep, SchemaFlowMenuStep, - SchemaOptionsFlowHandler, ) from homeassistant.helpers.selector import ( BooleanSelector, @@ -47,20 +59,15 @@ from homeassistant.helpers.selector import ( TextSelectorType, ) +from . import COMBINED_SCHEMA from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN -SCHEMA_SETUP = { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(), +RESOURCE_SETUP = { vol.Required(CONF_RESOURCE): TextSelector( TextSelectorConfig(type=TextSelectorType.URL) ), - vol.Required(CONF_SELECT): TextSelector(), -} - -SCHEMA_OPT = { - vol.Optional(CONF_ATTRIBUTE): TextSelector(), - vol.Optional(CONF_INDEX, default=0): NumberSelector( - NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): SelectSelector( + SelectSelectorConfig(options=METHODS, mode=SelectSelectorMode.DROPDOWN) ), vol.Optional(CONF_AUTHENTICATION): SelectSelector( SelectSelectorConfig( @@ -73,32 +80,199 @@ SCHEMA_OPT = { TextSelectorConfig(type=TextSelectorType.PASSWORD) ), vol.Optional(CONF_HEADERS): ObjectSelector(), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): TextSelector(), + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): BooleanSelector(), + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), +} + +SENSOR_SETUP = { + vol.Required(CONF_SELECT): TextSelector(), + vol.Optional(CONF_INDEX, default=0): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_ATTRIBUTE): TextSelector(), + vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(), vol.Optional(CONF_DEVICE_CLASS): SelectSelector( SelectSelectorConfig( - options=[e.value for e in SensorDeviceClass], + options=[cls.value for cls in SensorDeviceClass], mode=SelectSelectorMode.DROPDOWN, ) ), vol.Optional(CONF_STATE_CLASS): SelectSelector( SelectSelectorConfig( - options=[e.value for e in SensorStateClass], + options=[cls.value for cls in SensorStateClass], + mode=SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): SelectSelector( + SelectSelectorConfig( + options=[cls.value for cls in UnitOfTemperature], + custom_value=True, mode=SelectSelectorMode.DROPDOWN, ) ), - vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(), - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): BooleanSelector(), } -DATA_SCHEMA = vol.Schema({**SCHEMA_SETUP, **SCHEMA_OPT}) -DATA_SCHEMA_OPT = vol.Schema({**SCHEMA_OPT}) -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "user": SchemaFlowFormStep(DATA_SCHEMA), - "import": SchemaFlowFormStep(DATA_SCHEMA), +async def validate_rest_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate rest setup.""" + hass = async_get_hass() + rest_config: dict[str, Any] = COMBINED_SCHEMA(user_input) + try: + rest = create_rest_data_from_config(hass, rest_config) + await rest.async_update() + except Exception as err: + raise SchemaFlowError("resource_error") from err + if rest.data is None: + raise SchemaFlowError("resource_error") + return user_input + + +async def validate_sensor_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate sensor input.""" + user_input[CONF_INDEX] = int(user_input[CONF_INDEX]) + user_input[CONF_UNIQUE_ID] = str(uuid.uuid1()) + + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + sensors: list[dict[str, Any]] = handler.options.setdefault(SENSOR_DOMAIN, []) + sensors.append(user_input) + return {} + + +async def validate_select_sensor( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Store sensor index in flow state.""" + handler.flow_state["_idx"] = int(user_input[CONF_INDEX]) + return {} + + +async def get_select_sensor_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for selecting a sensor.""" + return vol.Schema( + { + vol.Required(CONF_INDEX): vol.In( + { + str(index): config[CONF_NAME] + for index, config in enumerate(handler.options[SENSOR_DOMAIN]) + }, + ) + } + ) + + +async def get_edit_sensor_suggested_values( + handler: SchemaCommonFlowHandler, +) -> dict[str, Any]: + """Return suggested values for sensor editing.""" + idx: int = handler.flow_state["_idx"] + return handler.options[SENSOR_DOMAIN][idx] + + +async def validate_sensor_edit( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Update edited sensor.""" + user_input[CONF_INDEX] = int(user_input[CONF_INDEX]) + + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + idx: int = handler.flow_state["_idx"] + handler.options[SENSOR_DOMAIN][idx].update(user_input) + return {} + + +async def get_remove_sensor_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for sensor removal.""" + return vol.Schema( + { + vol.Required(CONF_INDEX): cv.multi_select( + { + str(index): config[CONF_NAME] + for index, config in enumerate(handler.options[SENSOR_DOMAIN]) + }, + ) + } + ) + + +async def validate_remove_sensor( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate remove sensor.""" + removed_indexes: set[str] = set(user_input[CONF_INDEX]) + + # Standard behavior is to merge the result with the options. + # In this case, we want to remove sub-items so we update the options directly. + entity_registry = er.async_get(handler.parent_handler.hass) + sensors: list[dict[str, Any]] = [] + sensor: dict[str, Any] + for index, sensor in enumerate(handler.options[SENSOR_DOMAIN]): + if str(index) not in removed_indexes: + sensors.append(sensor) + elif entity_id := entity_registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, sensor[CONF_UNIQUE_ID] + ): + entity_registry.async_remove(entity_id) + handler.options[SENSOR_DOMAIN] = sensors + return {} + + +DATA_SCHEMA_RESOURCE = vol.Schema(RESOURCE_SETUP) +DATA_SCHEMA_EDIT_SENSOR = vol.Schema(SENSOR_SETUP) +DATA_SCHEMA_SENSOR = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(), + **SENSOR_SETUP, + } +) + +CONFIG_FLOW = { + "user": SchemaFlowFormStep( + schema=DATA_SCHEMA_RESOURCE, + next_step="sensor", + validate_user_input=validate_rest_setup, + ), + "sensor": SchemaFlowFormStep( + schema=DATA_SCHEMA_SENSOR, + validate_user_input=validate_sensor_setup, + ), } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(DATA_SCHEMA_OPT), +OPTIONS_FLOW = { + "init": SchemaFlowMenuStep( + ["resource", "add_sensor", "select_edit_sensor", "remove_sensor"] + ), + "resource": SchemaFlowFormStep( + DATA_SCHEMA_RESOURCE, + validate_user_input=validate_rest_setup, + ), + "add_sensor": SchemaFlowFormStep( + DATA_SCHEMA_SENSOR, + suggested_values=None, + validate_user_input=validate_sensor_setup, + ), + "select_edit_sensor": SchemaFlowFormStep( + get_select_sensor_schema, + suggested_values=None, + validate_user_input=validate_select_sensor, + next_step="edit_sensor", + ), + "edit_sensor": SchemaFlowFormStep( + DATA_SCHEMA_EDIT_SENSOR, + suggested_values=get_edit_sensor_suggested_values, + validate_user_input=validate_sensor_edit, + ), + "remove_sensor": SchemaFlowFormStep( + get_remove_sensor_schema, + suggested_values=None, + validate_user_input=validate_remove_sensor, + ), } @@ -110,13 +284,4 @@ class ScrapeConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" - return options[CONF_NAME] - - def async_config_flow_finished(self, options: Mapping[str, Any]) -> None: - """Check for duplicate records.""" - data: dict[str, Any] = dict(options) - self._async_abort_entries_match(data) - - -class ScrapeOptionsFlowHandler(SchemaOptionsFlowHandler): - """Handle a config flow for Scrape.""" + return options[CONF_RESOURCE] diff --git a/homeassistant/components/scrape/const.py b/homeassistant/components/scrape/const.py index 88eb661d29a..fc433ebb6f0 100644 --- a/homeassistant/components/scrape/const.py +++ b/homeassistant/components/scrape/const.py @@ -1,11 +1,14 @@ """Constants for Scrape integration.""" from __future__ import annotations +from datetime import timedelta + from homeassistant.const import Platform DOMAIN = "scrape" DEFAULT_NAME = "Web scrape" DEFAULT_VERIFY_SSL = True +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/scrape/coordinator.py b/homeassistant/components/scrape/coordinator.py index 3e81ba798ae..9fc66db3481 100644 --- a/homeassistant/components/scrape/coordinator.py +++ b/homeassistant/components/scrape/coordinator.py @@ -6,7 +6,7 @@ import logging from bs4 import BeautifulSoup -from homeassistant.components.rest.data import RestData +from homeassistant.components.rest import RestData from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -33,4 +33,6 @@ class ScrapeCoordinator(DataUpdateCoordinator[BeautifulSoup]): await self._rest.async_update() if (data := self._rest.data) is None: raise UpdateFailed("REST data is not available") - return await self.hass.async_add_executor_job(BeautifulSoup, data, "lxml") + soup = await self.hass.async_add_executor_job(BeautifulSoup, data, "lxml") + _LOGGER.debug("Raw beautiful soup: %s", soup) + return soup diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index b7e5660e381..86319ce3744 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -5,5 +5,6 @@ "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.1"], "after_dependencies": ["rest"], "codeowners": ["@fabaff", "@gjohansson-ST", "@epenet"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "config_flow": true } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b5ba471c301..e16f6c20f8d 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -5,18 +5,18 @@ from datetime import timedelta import logging from typing import Any -import httpx import voluptuous as vol -from homeassistant.components.rest.data import RestData +from homeassistant.components.rest import RESOURCE_SCHEMA, create_rest_data_from_config from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, - SensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_ATTRIBUTE, CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_HEADERS, @@ -36,42 +36,49 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import ( + TEMPLATE_SENSOR_BASE_SCHEMA, + TemplateSensor, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import ( + CONF_INDEX, + CONF_SELECT, + DEFAULT_NAME, + DEFAULT_SCAN_INTERVAL, + DEFAULT_VERIFY_SSL, + DOMAIN, +) from .coordinator import ScrapeCoordinator _LOGGER = logging.getLogger(__name__) -DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) - -CONF_ATTR = "attribute" -CONF_SELECT = "select" -CONF_INDEX = "index" - -DEFAULT_NAME = "Web scrape" -DEFAULT_VERIFY_SSL = True - PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { - vol.Required(CONF_RESOURCE): cv.string, - vol.Required(CONF_SELECT): cv.string, - vol.Optional(CONF_ATTR): cv.string, - vol.Optional(CONF_INDEX, default=0): cv.positive_int, + # Linked to the loading of the page (can be linked to RestData) vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Required(CONF_RESOURCE): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + # Linked to the parsing of the page (specific to scrape) + vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_INDEX, default=0): cv.positive_int, + vol.Required(CONF_SELECT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + # Linked to the sensor definition (can be linked to TemplateSensor) vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ) @@ -83,92 +90,133 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - name: str = config[CONF_NAME] - resource: str = config[CONF_RESOURCE] - method: str = "GET" - payload: str | None = None - headers: dict[str, str] | None = config.get(CONF_HEADERS) - verify_ssl: bool = config[CONF_VERIFY_SSL] - select: str | None = config.get(CONF_SELECT) - attr: str | None = config.get(CONF_ATTR) - index: int = config[CONF_INDEX] - unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) - device_class: str | None = config.get(CONF_DEVICE_CLASS) - state_class: str | None = config.get(CONF_STATE_CLASS) - unique_id: str | None = config.get(CONF_UNIQUE_ID) - username: str | None = config.get(CONF_USERNAME) - password: str | None = config.get(CONF_PASSWORD) - value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) + coordinator: ScrapeCoordinator + sensors_config: list[ConfigType] + if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "moved_yaml", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="moved_yaml", + ) + resource_config = vol.Schema(RESOURCE_SCHEMA, extra=vol.REMOVE_EXTRA)(config) + rest = create_rest_data_from_config(hass, resource_config) - if value_template is not None: - value_template.hass = hass + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + coordinator = ScrapeCoordinator(hass, rest, scan_interval) - auth: httpx.DigestAuth | tuple[str, str] | None = None - if username and password: - if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) + sensors_config = [ + vol.Schema(TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.ALLOW_EXTRA)( + config + ) + ] - rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) + else: + coordinator = discovery_info["coordinator"] + sensors_config = discovery_info["configs"] - scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - coordinator = ScrapeCoordinator(hass, rest, scan_interval) await coordinator.async_refresh() if coordinator.data is None: raise PlatformNotReady - async_add_entities( - [ + entities: list[ScrapeSensor] = [] + for sensor_config in sensors_config: + value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass + + entities.append( ScrapeSensor( + hass, coordinator, - unique_id, + sensor_config, + sensor_config[CONF_NAME], + sensor_config.get(CONF_UNIQUE_ID), + sensor_config[CONF_SELECT], + sensor_config.get(CONF_ATTRIBUTE), + sensor_config[CONF_INDEX], + value_template, + ) + ) + + async_add_entities(entities) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Scrape sensor entry.""" + entities: list = [] + + coordinator: ScrapeCoordinator = hass.data[DOMAIN][entry.entry_id] + config = dict(entry.options) + for sensor in config["sensor"]: + sensor_config: ConfigType = vol.Schema( + TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.ALLOW_EXTRA + )(sensor) + + name: str = sensor_config[CONF_NAME] + select: str = sensor_config[CONF_SELECT] + attr: str | None = sensor_config.get(CONF_ATTRIBUTE) + index: int = int(sensor_config[CONF_INDEX]) + value_string: str | None = sensor_config.get(CONF_VALUE_TEMPLATE) + unique_id: str = sensor_config[CONF_UNIQUE_ID] + + value_template: Template | None = ( + Template(value_string, hass) if value_string is not None else None + ) + entities.append( + ScrapeSensor( + hass, + coordinator, + sensor_config, name, + unique_id, select, attr, index, value_template, - unit, - device_class, - state_class, ) - ], - ) + ) + + async_add_entities(entities) -class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], SensorEntity): +class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): """Representation of a web scrape sensor.""" def __init__( self, + hass: HomeAssistant, coordinator: ScrapeCoordinator, - unique_id: str | None, + config: ConfigType, name: str, - select: str | None, + unique_id: str | None, + select: str, attr: str | None, index: int, value_template: Template | None, - unit: str | None, - device_class: str | None, - state_class: str | None, ) -> None: """Initialize a web scrape sensor.""" - super().__init__(coordinator) - self._attr_native_value = None + CoordinatorEntity.__init__(self, coordinator) + TemplateSensor.__init__( + self, + hass, + config=config, + fallback_name=name, + unique_id=unique_id, + ) self._select = select self._attr = attr self._index = index self._value_template = value_template - self._attr_name = name - self._attr_unique_id = unique_id - self._attr_native_unit_of_measurement = unit - self._attr_device_class = device_class - self._attr_state_class = state_class def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" raw_data = self.coordinator.data - _LOGGER.debug("Raw beautiful soup: %s", raw_data) try: if self._attr is not None: value = raw_data.select(self._select)[self._index][self._attr] diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index f328423f5b6..907aa2a9dfd 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -3,35 +3,48 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, + "error": { + "resource_error": "Could not update rest data. Verify your configuration" + }, "step": { "user": { "data": { - "name": "[%key:common::config_flow::data::name%]", "resource": "Resource", - "select": "Select", - "attribute": "Attribute", - "index": "Index", - "value_template": "Value Template", - "unit_of_measurement": "Unit of Measurement", - "device_class": "Device Class", - "state_class": "State Class", - "authentication": "Authentication", + "authentication": "Select authentication method", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "headers": "Headers" + "headers": "Headers", + "method": "Method", + "timeout": "Timeout" }, "data_description": { "resource": "The URL to the website that contains the value", + "authentication": "Type of the HTTP authentication. Either basic or digest", + "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed", + "headers": "Headers to use for the web request", + "timeout": "Timeout for connection to website" + } + }, + "sensor": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "attribute": "Attribute", + "index": "Index", + "select": "Select", + "value_template": "Value Template", + "device_class": "Device Class", + "state_class": "State Class", + "unit_of_measurement": "Unit of Measurement" + }, + "data_description": { "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", "attribute": "Get value of an attribute on the selected tag", "index": "Defines which of the elements returned by the CSS selector to use", "value_template": "Defines a template to get the state of the sensor", "device_class": "The type/class of the sensor to set the icon in the frontend", "state_class": "The state_class of the sensor", - "authentication": "Type of the HTTP authentication. Either basic or digest", - "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed", - "headers": "Headers to use for the web request" + "unit_of_measurement": "Choose temperature measurement or create your own" } } } @@ -39,35 +52,80 @@ "options": { "step": { "init": { + "menu_options": { + "add_sensor": "Add sensor", + "select_edit_sensor": "Configure sensor", + "remove_sensor": "Remove sensor", + "resource": "Configure resource" + } + }, + "add_sensor": { + "data": { + "name": "[%key:component::scrape::config::step::sensor::data::name%]", + "attribute": "[%key:component::scrape::config::step::sensor::data::attribute%]", + "index": "[%key:component::scrape::config::step::sensor::data::index%]", + "select": "[%key:component::scrape::config::step::sensor::data::select%]", + "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", + "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", + "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" + }, + "data_description": { + "select": "[%key:component::scrape::config::step::sensor::data_description::select%]", + "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", + "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", + "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", + "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", + "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" + } + }, + "edit_sensor": { + "data": { + "name": "[%key:component::scrape::config::step::sensor::data::name%]", + "attribute": "[%key:component::scrape::config::step::sensor::data::attribute%]", + "index": "[%key:component::scrape::config::step::sensor::data::index%]", + "select": "[%key:component::scrape::config::step::sensor::data::select%]", + "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", + "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", + "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" + }, + "data_description": { + "select": "[%key:component::scrape::config::step::sensor::data_description::select%]", + "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", + "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", + "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", + "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", + "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" + } + }, + "resource": { "data": { - "name": "[%key:component::scrape::config::step::user::data::name%]", "resource": "[%key:component::scrape::config::step::user::data::resource%]", - "select": "[%key:component::scrape::config::step::user::data::select%]", - "attribute": "[%key:component::scrape::config::step::user::data::attribute%]", - "index": "[%key:component::scrape::config::step::user::data::index%]", - "value_template": "[%key:component::scrape::config::step::user::data::value_template%]", - "unit_of_measurement": "[%key:component::scrape::config::step::user::data::unit_of_measurement%]", - "device_class": "[%key:component::scrape::config::step::user::data::device_class%]", - "state_class": "[%key:component::scrape::config::step::user::data::state_class%]", + "method": "[%key:component::scrape::config::step::user::data::method%]", "authentication": "[%key:component::scrape::config::step::user::data::authentication%]", - "verify_ssl": "[%key:component::scrape::config::step::user::data::verify_ssl%]", "username": "[%key:component::scrape::config::step::user::data::username%]", "password": "[%key:component::scrape::config::step::user::data::password%]", - "headers": "[%key:component::scrape::config::step::user::data::headers%]" + "headers": "[%key:component::scrape::config::step::user::data::headers%]", + "verify_ssl": "[%key:component::scrape::config::step::user::data::verify_ssl%]", + "timeout": "[%key:component::scrape::config::step::user::data::timeout%]" }, "data_description": { "resource": "[%key:component::scrape::config::step::user::data_description::resource%]", - "select": "[%key:component::scrape::config::step::user::data_description::select%]", - "attribute": "[%key:component::scrape::config::step::user::data_description::attribute%]", - "index": "[%key:component::scrape::config::step::user::data_description::index%]", - "value_template": "[%key:component::scrape::config::step::user::data_description::value_template%]", - "device_class": "[%key:component::scrape::config::step::user::data_description::device_class%]", - "state_class": "[%key:component::scrape::config::step::user::data_description::state_class%]", "authentication": "[%key:component::scrape::config::step::user::data_description::authentication%]", + "headers": "[%key:component::scrape::config::step::user::data_description::headers%]", "verify_ssl": "[%key:component::scrape::config::step::user::data_description::verify_ssl%]", - "headers": "[%key:component::scrape::config::step::user::data_description::headers%]" + "timeout": "[%key:component::scrape::config::step::user::data_description::timeout%]" } } } + }, + "issues": { + "moved_yaml": { + "title": "The Scrape YAML configuration has been moved", + "description": "Configuring Scrape using YAML has been moved to integration key.\n\nYour existing YAML configuration will be working for 2 more versions.\n\nMigrate your YAML configuration to the integration key according to the documentation." + } } } diff --git a/homeassistant/components/scrape/translations/bg.json b/homeassistant/components/scrape/translations/bg.json index 1599a1918d7..059fe59152c 100644 --- a/homeassistant/components/scrape/translations/bg.json +++ b/homeassistant/components/scrape/translations/bg.json @@ -4,31 +4,40 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "step": { - "user": { + "sensor": { "data": { "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", - "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u0418\u043c\u0435", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435", - "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435", + "unit_of_measurement": "\u041c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "data_description": { + "state_class": "state_class \u043d\u0430 \u0441\u0435\u043d\u0437\u043e\u0440\u0430" + } + }, + "user": { + "data": { + "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "method": "\u041c\u0435\u0442\u043e\u0434", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } }, + "issues": { + "moved_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Scrape \u0435 \u043f\u0440\u0435\u043c\u0435\u0441\u0442\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { "data": { - "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", - "index": "\u0418\u043d\u0434\u0435\u043a\u0441", - "name": "\u0418\u043c\u0435", + "method": "\u041c\u0435\u0442\u043e\u0434", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435", - "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/scrape/translations/ca.json b/homeassistant/components/scrape/translations/ca.json index ff6a0dba168..5b3688f2e2d 100644 --- a/homeassistant/components/scrape/translations/ca.json +++ b/homeassistant/components/scrape/translations/ca.json @@ -3,68 +3,117 @@ "abort": { "already_configured": "El compte ja est\u00e0 configurat" }, + "error": { + "resource_error": "No s'han pogut actualitzar les dades restants. Verifica la configuraci\u00f3" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Atribut", - "authentication": "Autenticaci\u00f3", "device_class": "Classe de dispositiu", - "headers": "Cap\u00e7aleres", "index": "\u00cdndex", "name": "Nom", - "password": "Contrasenya", - "resource": "Recurs", "select": "Selecciona", "state_class": "Classe d'estat", "unit_of_measurement": "Unitat de mesura", - "username": "Nom d'usuari", - "value_template": "Plantilla de valor", - "verify_ssl": "Verifica el certificat SSL" + "value_template": "Plantilla de valor" }, "data_description": { "attribute": "Obt\u00e9 el valor d'un atribut de l'etiqueta seleccionada", - "authentication": "Tipus d'autenticaci\u00f3 HTTP. O b\u00e0sica o 'digest'", - "device_class": "Tipus/classe del sensor per configurar-ne la icona a la interf\u00edcie", - "headers": "Cap\u00e7aleres a utilitzar per a la sol\u00b7licitud web", + "device_class": "Tipus/classe del sensor per configurar-ne la icona a la interf\u00edcie d'usuari", "index": "Defineix quins dels elements retornats pel selector CSS utilitzar", - "resource": "URL del lloc web que cont\u00e9 el valor", "select": "Defineix quina etiqueta s'ha de buscar. Consulta els selectors CSS de Beautifulsoup per m\u00e9s informaci\u00f3", - "state_class": "La state_class del sensor", - "value_template": "Defineix una plantilla per obtenir l'estat del sensor", + "state_class": "'state_class' del sensor", + "unit_of_measurement": "Tria la unitat de mesura de temperatura o crea la teva", + "value_template": "Plantilla per obtenir l'estat del sensor" + } + }, + "user": { + "data": { + "authentication": "Autenticaci\u00f3", + "headers": "Cap\u00e7aleres", + "method": "M\u00e8tode", + "password": "Contrasenya", + "resource": "Recurs", + "timeout": "Temps d'espera", + "username": "Nom d'usuari", + "verify_ssl": "Verifica el certificat SSL" + }, + "data_description": { + "authentication": "Tipus d'autenticaci\u00f3 HTTP. O b\u00e0sica o 'digest'", + "headers": "Cap\u00e7aleres a utilitzar per a la sol\u00b7licitud web", + "resource": "URL del lloc web que cont\u00e9 el valor", + "timeout": "Temps d'espera de connexi\u00f3 al lloc web", "verify_ssl": "Activa/desactiva la verificaci\u00f3 del certificat SSL/TLS, per exemple, si est\u00e0 autosignat" } } } }, + "issues": { + "moved_yaml": { + "description": "La configuraci\u00f3 de Scrape mitjan\u00e7ant YAML s'ha mogut a una integraci\u00f3.\n\nLa configuraci\u00f3 YAML actual continuar\u00e0 funcionant en les dues pr\u00f2ximes versions de Home Assistant.\n\nMigra la teva configuraci\u00f3 YAML a la nova integraci\u00f3, consulta la documentaci\u00f3 per saber com fer-ho.", + "title": "La configuraci\u00f3 YAML de Scrape s'ha eliminat" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "Atribut", - "authentication": "Autenticaci\u00f3", "device_class": "Classe de dispositiu", - "headers": "Cap\u00e7aleres", "index": "\u00cdndex", - "name": "Nom", - "password": "Contrasenya", - "resource": "Recurs", "select": "Selecciona", "state_class": "Classe d'estat", "unit_of_measurement": "Unitat de mesura", - "username": "Nom d'usuari", - "value_template": "Plantilla de valor", - "verify_ssl": "Verifica el certificat SSL" + "value_template": "Plantilla de valor" }, "data_description": { "attribute": "Obt\u00e9 el valor d'un atribut de l'etiqueta seleccionada", - "authentication": "Tipus d'autenticaci\u00f3 HTTP. O b\u00e0sica o 'digest'", - "device_class": "Tipus/classe del sensor per configurar-ne la icona a la interf\u00edcie", - "headers": "Cap\u00e7aleres a utilitzar per a la sol\u00b7licitud web", + "device_class": "Tipus/classe del sensor per configurar-ne la icona a la interf\u00edcie d'usuari", "index": "Defineix quins dels elements retornats pel selector CSS utilitzar", - "resource": "URL del lloc web que cont\u00e9 el valor", "select": "Defineix quina etiqueta s'ha de buscar. Consulta els selectors CSS de Beautifulsoup per m\u00e9s informaci\u00f3", - "state_class": "La state_class del sensor", - "value_template": "Defineix una plantilla per obtenir l'estat del sensor", + "state_class": "'state_class' del sensor", + "unit_of_measurement": "Tria la unitat de mesura de temperatura o crea la teva", + "value_template": "Plantilla per obtenir l'estat del sensor" + } + }, + "init": { + "data": { + "authentication": "Autenticaci\u00f3", + "headers": "Cap\u00e7aleres", + "method": "M\u00e8tode", + "password": "Contrasenya", + "resource": "Recurs", + "timeout": "Temps d'espera", + "username": "Nom d'usuari", + "verify_ssl": "Verifica el certificat SSL" + }, + "data_description": { + "authentication": "Tipus d'autenticaci\u00f3 HTTP. O b\u00e0sica o 'digest'", + "headers": "Cap\u00e7aleres a utilitzar per a la sol\u00b7licitud web", + "resource": "URL del lloc web que cont\u00e9 el valor", + "timeout": "Temps d'espera de connexi\u00f3 al lloc web", + "verify_ssl": "Activa/desactiva la verificaci\u00f3 del certificat SSL/TLS, per exemple, si est\u00e0 autosignat" + }, + "menu_options": { + "add_sensor": "Afegeix sensor", + "remove_sensor": "Elimina sensor", + "resource": "Configura recurs" + } + }, + "resource": { + "data": { + "authentication": "Autenticaci\u00f3", + "headers": "Cap\u00e7aleres", + "method": "M\u00e8tode", + "resource": "Recurs", + "timeout": "Temps d'espera" + }, + "data_description": { + "authentication": "Tipus d'autenticaci\u00f3 HTTP. O b\u00e0sica o 'digest'", + "headers": "Cap\u00e7aleres a utilitzar per a la sol\u00b7licitud web", + "resource": "URL del lloc web que cont\u00e9 el valor", + "timeout": "Temps d'espera de connexi\u00f3 al lloc web", "verify_ssl": "Activa/desactiva la verificaci\u00f3 del certificat SSL/TLS, per exemple, si est\u00e0 autosignat" } } diff --git a/homeassistant/components/scrape/translations/cs.json b/homeassistant/components/scrape/translations/cs.json index 6c5458481b8..68cbf40a518 100644 --- a/homeassistant/components/scrape/translations/cs.json +++ b/homeassistant/components/scrape/translations/cs.json @@ -3,12 +3,53 @@ "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven" }, + "error": { + "resource_error": "Nepoda\u0159ilo se aktualizovat rest data. Ov\u011b\u0159te svou konfiguraci" + }, "step": { + "sensor": { + "data": { + "attribute": "Atribut", + "device_class": "T\u0159\u00edda za\u0159\u00edzen\u00ed", + "index": "Index", + "name": "Jm\u00e9no", + "select": "Vybrat", + "unit_of_measurement": "Jednotka m\u011b\u0159en\u00ed", + "value_template": "\u0160ablona hodnoty" + }, + "data_description": { + "attribute": "Z\u00edskat hodnotu atributu na vybran\u00e9m tagu", + "device_class": "Typ/t\u0159\u00edda senzoru pro nastaven\u00ed ikony v rozhran\u00ed", + "index": "Definuje, kter\u00fd z prvk\u016f vr\u00e1cen\u00fdch selektorem CSS se m\u00e1 pou\u017e\u00edt", + "select": "Definuje, jak\u00fd tag hledat. Podrobnosti najdete v selektorech CSS Beautifulsoup", + "state_class": "state_class senzoru", + "unit_of_measurement": "Zvolte m\u011b\u0159en\u00ed teploty nebo si vytvo\u0159te vlastn\u00ed", + "value_template": "Definuje \u0161ablonu pro z\u00edsk\u00e1n\u00ed stavu senzoru" + } + }, "user": { "data": { "headers": "Hlavi\u010dky", + "method": "Metoda", "password": "Heslo", + "timeout": "\u010casov\u00fd limit", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "data_description": { + "timeout": "\u010casov\u00fd limit pro p\u0159ipojen\u00ed k webu" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "method": "Metoda", + "timeout": "\u010casov\u00fd limit" + }, + "data_description": { + "timeout": "\u010casov\u00fd limit pro p\u0159ipojen\u00ed k webu" } } } diff --git a/homeassistant/components/scrape/translations/de.json b/homeassistant/components/scrape/translations/de.json index d4e2f37f88d..9deb92dec32 100644 --- a/homeassistant/components/scrape/translations/de.json +++ b/homeassistant/components/scrape/translations/de.json @@ -3,68 +3,121 @@ "abort": { "already_configured": "Konto wurde bereits konfiguriert" }, + "error": { + "resource_error": "Restdaten konnten nicht aktualisiert werden. \u00dcberpr\u00fcfe deine Konfiguration" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Attribut", - "authentication": "Authentifizierung", "device_class": "Ger\u00e4teklasse", - "headers": "Header", "index": "Index", "name": "Name", - "password": "Passwort", - "resource": "Ressource", "select": "Ausw\u00e4hlen", "state_class": "Zustandsklasse", "unit_of_measurement": "Ma\u00dfeinheit", - "username": "Benutzername", - "value_template": "Wertvorlage", - "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + "value_template": "Wertvorlage" }, "data_description": { "attribute": "Wert eines Attributs auf dem ausgew\u00e4hlten Tag abrufen", - "authentication": "Typ der HTTP-Authentifizierung. Entweder basic oder digest", "device_class": "Der Typ/die Klasse des Sensors, um das Symbol im Frontend festzulegen", - "headers": "F\u00fcr die Webanforderung zu verwendende Header", "index": "Definiert, welche der vom CSS-Selektor zur\u00fcckgegebenen Elemente verwendet werden sollen", - "resource": "Die URL der Website, die den Wert enth\u00e4lt", "select": "Legt fest, nach welchem Tag gesucht werden soll. Siehe Beautifulsoup CSS-Selektoren f\u00fcr Details", "state_class": "Die state_class des Sensors", - "value_template": "Definiert eine Vorlage, um den Zustand des Sensors zu ermitteln", + "unit_of_measurement": "W\u00e4hle die Temperaturmessung oder erstelle deine eigene", + "value_template": "Definiert eine Vorlage, um den Zustand des Sensors zu ermitteln" + } + }, + "user": { + "data": { + "authentication": "Authentifizierungsmethode ausw\u00e4hlen", + "headers": "Header", + "method": "Methode", + "password": "Passwort", + "resource": "Ressource", + "timeout": "Zeit\u00fcberschreitung", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "data_description": { + "authentication": "Typ der HTTP-Authentifizierung. Entweder basic oder digest", + "headers": "F\u00fcr die Webanforderung zu verwendende Header", + "resource": "Die URL der Website, die den Wert enth\u00e4lt", + "timeout": "Zeit\u00fcberschreitung f\u00fcr die Verbindung zur Website", "verify_ssl": "Aktiviert/deaktiviert die \u00dcberpr\u00fcfung des SSL/TLS-Zertifikats, z.B. wenn es selbst signiert ist" } } } }, + "issues": { + "moved_yaml": { + "description": "Die Konfiguration von Scrape mit YAML wurde in den Integrationsschl\u00fcssel verschoben. \n\nDeine vorhandene YAML-Konfiguration funktioniert f\u00fcr zwei weitere Versionen.\n\nMigriere deine YAML-Konfiguration gem\u00e4\u00df der Dokumentation zum Integrationsschl\u00fcssel.", + "title": "Die Scrape YAML-Konfiguration wurde verschoben" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "Attribut", - "authentication": "Authentifizierung", "device_class": "Ger\u00e4teklasse", - "headers": "Header", "index": "Index", "name": "Name", - "password": "Passwort", - "resource": "Ressource", "select": "Ausw\u00e4hlen", "state_class": "Zustandsklasse", "unit_of_measurement": "Ma\u00dfeinheit", - "username": "Benutzername", - "value_template": "Wertvorlage", - "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + "value_template": "Wertvorlage" }, "data_description": { "attribute": "Wert eines Attributs auf dem ausgew\u00e4hlten Tag abrufen", - "authentication": "Typ der HTTP-Authentifizierung. Entweder basic oder digest", "device_class": "Der Typ/die Klasse des Sensors, um das Symbol im Frontend festzulegen", - "headers": "F\u00fcr die Webanforderung zu verwendende Header", "index": "Definiert, welche der vom CSS-Selektor zur\u00fcckgegebenen Elemente verwendet werden sollen", - "resource": "Die URL der Website, die den Wert enth\u00e4lt", "select": "Legt fest, nach welchem Tag gesucht werden soll. Siehe Beautifulsoup CSS-Selektoren f\u00fcr Details", "state_class": "Die state_class des Sensors", - "value_template": "Definiert eine Vorlage, um den Zustand des Sensors zu ermitteln", + "unit_of_measurement": "W\u00e4hle die Temperaturmessung oder erstelle deine eigene", + "value_template": "Definiert eine Vorlage, um den Zustand des Sensors zu ermitteln" + } + }, + "init": { + "data": { + "authentication": "Authentifizierungsmethode ausw\u00e4hlen", + "headers": "Header", + "method": "Methode", + "password": "Passwort", + "resource": "Ressource", + "timeout": "Zeit\u00fcberschreitung", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "data_description": { + "authentication": "Typ der HTTP-Authentifizierung. Entweder basic oder digest", + "headers": "F\u00fcr die Webanforderung zu verwendende Header", + "resource": "Die URL der Website, die den Wert enth\u00e4lt", + "timeout": "Zeit\u00fcberschreitung f\u00fcr die Verbindung zur Website", + "verify_ssl": "Aktiviert/deaktiviert die \u00dcberpr\u00fcfung des SSL/TLS-Zertifikats, z.B. wenn es selbst signiert ist" + }, + "menu_options": { + "add_sensor": "Sensor hinzuf\u00fcgen", + "remove_sensor": "Sensor entfernen", + "resource": "Ressource konfigurieren" + } + }, + "resource": { + "data": { + "authentication": "Authentifizierungsmethode ausw\u00e4hlen", + "headers": "Header", + "method": "Methode", + "password": "Passwort", + "resource": "Ressource", + "timeout": "Zeit\u00fcberschreitung", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "data_description": { + "authentication": "Typ der HTTP-Authentifizierung. Entweder basic oder digest", + "headers": "F\u00fcr die Webanforderung zu verwendende Header", + "resource": "Die URL der Website, die den Wert enth\u00e4lt", + "timeout": "Zeit\u00fcberschreitung f\u00fcr die Verbindung zur Website", "verify_ssl": "Aktiviert/deaktiviert die \u00dcberpr\u00fcfung des SSL/TLS-Zertifikats, z.B. wenn es selbst signiert ist" } } diff --git a/homeassistant/components/scrape/translations/el.json b/homeassistant/components/scrape/translations/el.json index 8782b2b9f6d..b516f9b37d7 100644 --- a/homeassistant/components/scrape/translations/el.json +++ b/homeassistant/components/scrape/translations/el.json @@ -3,68 +3,76 @@ "abort": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, + "error": { + "resource_error": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b1\u03bd\u03ac\u03c0\u03b1\u03c5\u03c3\u03b7\u03c2. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "\u03a7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", - "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "device_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", - "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2", "index": "\u0394\u03b5\u03af\u03ba\u03c4\u03b7\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "resource": "\u03a0\u03cc\u03c1\u03bf\u03c2", "select": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae", "state_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", - "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2", - "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2 " }, "data_description": { "attribute": "\u039b\u03ae\u03c8\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1", - "authentication": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 HTTP. \u0395\u03af\u03c4\u03b5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03c4\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "device_class": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2/\u03ba\u03bb\u03ac\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc \u03c4\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5 \u03c3\u03c4\u03bf frontend", + "index": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03b1 CSS \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7", + "select": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03b5\u03af\u03c2 CSS Beautifulsoup \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", + "state_class": "\u0397 state_class \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1", + "unit_of_measurement": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ae \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03ba\u03ae \u03c3\u03b1\u03c2", + "value_template": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + } + }, + "user": { + "data": { + "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2", + "method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "resource": "\u03a0\u03cc\u03c1\u03bf\u03c2", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "data_description": { + "authentication": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 HTTP. \u0395\u03af\u03c4\u03b5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03c4\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b9\u03c3\u03c4\u03bf\u03cd", - "index": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03b1 CSS \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd", "resource": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03bf\u03bd \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae", - "select": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03b5\u03af\u03c2 CSS \u03c4\u03bf\u03c5 Beautifulsoup \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", - "state_class": "\u0397 state_class \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2", - "value_template": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03bb\u03b1\u03b2\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf", "verify_ssl": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af/\u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL/TLS, \u03c0.\u03c7. \u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c5\u03c4\u03bf-\u03c5\u03c0\u03bf\u03b3\u03b5\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf." } } } }, + "issues": { + "moved_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Scrape \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf YAML \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03b8\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03b3\u03b9\u03b1 2 \u03b1\u03ba\u03cc\u03bc\u03b7 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2. \n\n \u039c\u03b5\u03c4\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c3\u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Scrape YAML \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03b7\u03b8\u03b5\u03af" + } + }, "options": { "step": { "init": { "data": { - "attribute": "\u03a7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "device_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2", - "index": "\u0394\u03b5\u03af\u03ba\u03c4\u03b7\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2", "password": "\u039a\u03b5\u03bd\u03cc", "resource": "\u03a0\u03cc\u03c1\u03bf\u03c2", - "select": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae", - "state_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", - "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf", "username": "\u039a\u03b5\u03bd\u03cc", - "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" }, "data_description": { - "attribute": "\u039b\u03ae\u03c8\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1", "authentication": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 HTTP. \u0395\u03af\u03c4\u03b5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03c4\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", - "device_class": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2/\u03ba\u03bb\u03ac\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc \u03c4\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5 \u03c3\u03c4\u03bf frontend", "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b9\u03c3\u03c4\u03bf\u03cd", - "index": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03b1 CSS \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd", "resource": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03bf\u03bd \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae", - "select": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03b5\u03af\u03c2 CSS \u03c4\u03bf\u03c5 Beautifulsoup \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", - "state_class": "\u0397 state_class \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1", - "value_template": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03bb\u03b1\u03b2\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf", "verify_ssl": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af/\u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL/TLS, \u03c0.\u03c7. \u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c5\u03c4\u03bf-\u03c5\u03c0\u03bf\u03b3\u03b5\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf." } } diff --git a/homeassistant/components/scrape/translations/en.json b/homeassistant/components/scrape/translations/en.json index 20831f5251a..4b0e96da680 100644 --- a/homeassistant/components/scrape/translations/en.json +++ b/homeassistant/components/scrape/translations/en.json @@ -3,68 +3,126 @@ "abort": { "already_configured": "Account is already configured" }, + "error": { + "resource_error": "Could not update rest data. Verify your configuration" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Attribute", - "authentication": "Authentication", "device_class": "Device Class", - "headers": "Headers", "index": "Index", "name": "Name", - "password": "Password", - "resource": "Resource", "select": "Select", "state_class": "State Class", "unit_of_measurement": "Unit of Measurement", - "username": "Username", - "value_template": "Value Template", - "verify_ssl": "Verify SSL certificate" + "value_template": "Value Template" }, "data_description": { "attribute": "Get value of an attribute on the selected tag", - "authentication": "Type of the HTTP authentication. Either basic or digest", "device_class": "The type/class of the sensor to set the icon in the frontend", - "headers": "Headers to use for the web request", "index": "Defines which of the elements returned by the CSS selector to use", - "resource": "The URL to the website that contains the value", "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", "state_class": "The state_class of the sensor", - "value_template": "Defines a template to get the state of the sensor", + "unit_of_measurement": "Choose temperature measurement or create your own", + "value_template": "Defines a template to get the state of the sensor" + } + }, + "user": { + "data": { + "authentication": "Select authentication method", + "headers": "Headers", + "method": "Method", + "password": "Password", + "resource": "Resource", + "timeout": "Timeout", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + }, + "data_description": { + "authentication": "Type of the HTTP authentication. Either basic or digest", + "headers": "Headers to use for the web request", + "resource": "The URL to the website that contains the value", + "timeout": "Timeout for connection to website", "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed" } } } }, + "issues": { + "moved_yaml": { + "description": "Configuring Scrape using YAML has been moved to integration key.\n\nYour existing YAML configuration will be working for 2 more versions.\n\nMigrate your YAML configuration to the integration key according to the documentation.", + "title": "The Scrape YAML configuration has been moved" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "Attribute", - "authentication": "Authentication", "device_class": "Device Class", - "headers": "Headers", "index": "Index", "name": "Name", - "password": "Password", - "resource": "Resource", "select": "Select", "state_class": "State Class", "unit_of_measurement": "Unit of Measurement", - "username": "Username", - "value_template": "Value Template", - "verify_ssl": "Verify SSL certificate" + "value_template": "Value Template" }, "data_description": { "attribute": "Get value of an attribute on the selected tag", - "authentication": "Type of the HTTP authentication. Either basic or digest", "device_class": "The type/class of the sensor to set the icon in the frontend", - "headers": "Headers to use for the web request", "index": "Defines which of the elements returned by the CSS selector to use", - "resource": "The URL to the website that contains the value", "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", "state_class": "The state_class of the sensor", - "value_template": "Defines a template to get the state of the sensor", + "unit_of_measurement": "Choose temperature measurement or create your own", + "value_template": "Defines a template to get the state of the sensor" + } + }, + "edit_sensor": { + "data": { + "attribute": "Attribute", + "device_class": "Device Class", + "index": "Index", + "name": "Name", + "select": "Select", + "state_class": "State Class", + "unit_of_measurement": "Unit of Measurement", + "value_template": "Value Template" + }, + "data_description": { + "attribute": "Get value of an attribute on the selected tag", + "device_class": "The type/class of the sensor to set the icon in the frontend", + "index": "Defines which of the elements returned by the CSS selector to use", + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", + "state_class": "The state_class of the sensor", + "unit_of_measurement": "Choose temperature measurement or create your own", + "value_template": "Defines a template to get the state of the sensor" + } + }, + "init": { + "menu_options": { + "add_sensor": "Add sensor", + "select_edit_sensor": "Configure sensor", + "remove_sensor": "Remove sensor", + "resource": "Configure resource" + } + }, + "resource": { + "data": { + "authentication": "Select authentication method", + "headers": "Headers", + "method": "Method", + "password": "Password", + "resource": "Resource", + "timeout": "Timeout", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + }, + "data_description": { + "authentication": "Type of the HTTP authentication. Either basic or digest", + "headers": "Headers to use for the web request", + "resource": "The URL to the website that contains the value", + "timeout": "Timeout for connection to website", "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed" } } diff --git a/homeassistant/components/scrape/translations/es.json b/homeassistant/components/scrape/translations/es.json index d88197473d3..a53e6545a54 100644 --- a/homeassistant/components/scrape/translations/es.json +++ b/homeassistant/components/scrape/translations/es.json @@ -3,68 +3,121 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada" }, + "error": { + "resource_error": "No se pudieron actualizar los datos rest. Verifica tu configuraci\u00f3n" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Atributo", - "authentication": "Autenticaci\u00f3n", "device_class": "Clase de dispositivo", - "headers": "Cabeceras", "index": "\u00cdndice", "name": "Nombre", - "password": "Contrase\u00f1a", - "resource": "Recurso", "select": "Seleccionar", "state_class": "Clase de estado", "unit_of_measurement": "Unidad de medida", - "username": "Nombre de usuario", - "value_template": "Plantilla de valor", - "verify_ssl": "Verificar el certificado SSL" + "value_template": "Plantilla de valor" }, "data_description": { "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", - "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", - "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", - "headers": "Cabeceras a usar para la petici\u00f3n web", - "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS se va a usar", - "resource": "La URL del sitio web que contiene el valor.", + "device_class": "El tipo/clase del sensor para establecer el icono en la interfaz", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", "select": "Define qu\u00e9 etiqueta buscar. Revisa los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", - "value_template": "Define una plantilla para obtener el estado del sensor", + "unit_of_measurement": "Elige la medici\u00f3n de temperatura o crea la suya propia", + "value_template": "Define una plantilla para obtener el estado del sensor" + } + }, + "user": { + "data": { + "authentication": "Selecciona el m\u00e9todo de autenticaci\u00f3n", + "headers": "Cabeceras", + "method": "M\u00e9todo", + "password": "Contrase\u00f1a", + "resource": "Recurso", + "timeout": "Tiempo de espera", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" + }, + "data_description": { + "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", + "headers": "Cabeceras a usar para la petici\u00f3n web", + "resource": "La URL del sitio web que contiene el valor.", + "timeout": "Tiempo de espera para la conexi\u00f3n al sitio web", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" } } } }, + "issues": { + "moved_yaml": { + "description": "La configuraci\u00f3n de Scrape usando YAML se ha movido a la clave de integraci\u00f3n. \n\nTu configuraci\u00f3n YAML existente funcionar\u00e1 durante 2 versiones m\u00e1s. \n\nMigra tu configuraci\u00f3n YAML a la clave de integraci\u00f3n de acuerdo con la documentaci\u00f3n.", + "title": "La configuraci\u00f3n YAML de Scrape se ha movido" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "Atributo", - "authentication": "Autenticaci\u00f3n", "device_class": "Clase de dispositivo", - "headers": "Cabeceras", "index": "\u00cdndice", "name": "Nombre", - "password": "Contrase\u00f1a", - "resource": "Recurso", "select": "Seleccionar", "state_class": "Clase de estado", "unit_of_measurement": "Unidad de medida", - "username": "Nombre de usuario", - "value_template": "Plantilla de valor", - "verify_ssl": "Verificar el certificado SSL" + "value_template": "Plantilla de valor" }, "data_description": { "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", - "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", - "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", - "headers": "Cabeceras a usar para la petici\u00f3n web", - "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS se va a usar", - "resource": "La URL del sitio web que contiene el valor.", + "device_class": "El tipo/clase del sensor para establecer el icono en la interfaz", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", "select": "Define qu\u00e9 etiqueta buscar. Revisa los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", - "value_template": "Define una plantilla para obtener el estado del sensor", + "unit_of_measurement": "Elige la medici\u00f3n de temperatura o crea la suya propia", + "value_template": "Define una plantilla para obtener el estado del sensor" + } + }, + "init": { + "data": { + "authentication": "Selecciona el m\u00e9todo de autenticaci\u00f3n", + "headers": "Cabeceras", + "method": "M\u00e9todo", + "password": "Contrase\u00f1a", + "resource": "Recurso", + "timeout": "Tiempo de espera", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" + }, + "data_description": { + "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", + "headers": "Cabeceras a usar para la petici\u00f3n web", + "resource": "La URL del sitio web que contiene el valor.", + "timeout": "Tiempo de espera para la conexi\u00f3n al sitio web", + "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" + }, + "menu_options": { + "add_sensor": "A\u00f1adir sensor", + "remove_sensor": "Eliminar sensor", + "resource": "Configurar recurso" + } + }, + "resource": { + "data": { + "authentication": "Selecciona el m\u00e9todo de autenticaci\u00f3n", + "headers": "Cabeceras", + "method": "M\u00e9todo", + "password": "Contrase\u00f1a", + "resource": "Recurso", + "timeout": "Tiempo de espera", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" + }, + "data_description": { + "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", + "headers": "Cabeceras a usar para la petici\u00f3n web", + "resource": "La URL del sitio web que contiene el valor.", + "timeout": "Tiempo de espera para la conexi\u00f3n al sitio web", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" } } diff --git a/homeassistant/components/scrape/translations/et.json b/homeassistant/components/scrape/translations/et.json index 14daf835af8..bd60ffcbcc0 100644 --- a/homeassistant/components/scrape/translations/et.json +++ b/homeassistant/components/scrape/translations/et.json @@ -3,69 +3,122 @@ "abort": { "already_configured": "Kasutaja on juba seadistatud" }, + "error": { + "resource_error": "Rest p\u00e4ringu andmeid ei saanud v\u00e4rskendada. Kontrolli oma s\u00e4tteid" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Atribuut", - "authentication": "Tuvastamine", "device_class": "Seadme klass", - "headers": "P\u00e4ised", "index": "Indeks", "name": "Nimi", - "password": "Salas\u00f5na", - "resource": "Resurss", "select": "Vali", "state_class": "Oleku klass", "unit_of_measurement": "M\u00f5\u00f5t\u00fchik", - "username": "Kasutajanimi", - "value_template": "V\u00e4\u00e4rtuse mall", - "verify_ssl": "Kontrolli SSL serti" + "value_template": "V\u00e4\u00e4rtuse mall" }, "data_description": { "attribute": "Hangi valitud sildi atribuudi v\u00e4\u00e4rtus", - "authentication": "HTTP-autentimise t\u00fc\u00fcp. Kas basic v\u00f5i digest", "device_class": "Anduri t\u00fc\u00fcp/klass ikooni seadmiseks kasutajaliideses", - "headers": "Veebip\u00e4ringu jaoks kasutatavad p\u00e4ised", "index": "M\u00e4\u00e4rab, milliseid CSS selektoriga tagastatud elemente kasutada.", - "resource": "V\u00e4\u00e4rtust sisaldava veebisaidi URL", "select": "M\u00e4\u00e4rab, millist silti otsida. Lisateavet leiad Beautifulsoup CSS-i valijatest", "state_class": "Anduri oleku klass", - "value_template": "M\u00e4\u00e4rab malli anduri oleku saamiseks", + "unit_of_measurement": "Vali temperatuuri m\u00f5\u00f5tmine v\u00f5i loo oma", + "value_template": "M\u00e4\u00e4rab malli anduri oleku saamiseks" + } + }, + "user": { + "data": { + "authentication": "Vali tuvastusmeetod", + "headers": "P\u00e4ised", + "method": "Meetod", + "password": "Salas\u00f5na", + "resource": "Resurss", + "timeout": "Ajal\u00f5pp", + "username": "Kasutajanimi", + "verify_ssl": "Kontrolli SSL serti" + }, + "data_description": { + "authentication": "HTTP-autentimise t\u00fc\u00fcp. Kas basic v\u00f5i digest", + "headers": "Veebip\u00e4ringu jaoks kasutatavad p\u00e4ised", + "resource": "V\u00e4\u00e4rtust sisaldava veebisaidi URL", + "timeout": "Veebilehe ajal\u00f5pp", "verify_ssl": "Lubab/keelab SSL/TLS-sertifikaadi kontrollimise, n\u00e4iteks kui see on ise allkirjastatud" } } } }, + "issues": { + "moved_yaml": { + "description": "Scrape'i konfigureerimine YAML-i abil on viidud integratsiooniv\u00f5tmesse.\n\nOlemasolev YAML-konfiguratsioon t\u00f6\u00f6tab veel 2 versiooni.\n\nMigreeri YAML-konfiguratsioon integratsiooniv\u00f5tmesse vastavalt dokumentatsioonile.", + "title": "Scrape YAML-i konfiguratsioon on teisaldatud" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { - "attribute": "Atribuut", - "authentication": "Tuvastamine", - "device_class": "Seadme klss", - "headers": "P\u00e4ised", + "attribute": "Attribuut", + "device_class": "Seadme klass", "index": "Indeks", "name": "", + "select": "Vali", + "state_class": "Olekuklass", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik", + "value_template": "V\u00e4\u00e4rtuse mall" + }, + "data_description": { + "attribute": "Hangi valitud m\u00e4rgise attribuudi v\u00e4\u00e4rtus", + "device_class": "Kuvatava ikooni t\u00fc\u00fcp/klass", + "index": "M\u00e4\u00e4rab milliseid CSS m\u00e4rgise esitatud elemente kasutada.", + "select": "Valib otsitava m\u00e4rgise. T\u00e4psem teave Beatifulsoup CSS valikutes.", + "state_class": "Anduri olekuklass", + "unit_of_measurement": "Vali temperatuuri m\u00f5\u00f5tmine v\u00f5i loo uus", + "value_template": "M\u00e4\u00e4rab malli anduri v\u00e4\u00e4rtuse jaoks" + } + }, + "init": { + "data": { + "authentication": "Tuvastamine", + "headers": "P\u00e4ised", + "method": "Meetod", "password": "Salas\u00f5na", "resource": "Resurss", - "select": "Vali", - "state_class": "Oleku klass", - "unit_of_measurement": "M\u00f5\u00f5t\u00fchik", + "timeout": "Ajal\u00f5pp", "username": "", - "value_template": "V\u00e4\u00e4rtuse mall", "verify_ssl": "" }, "data_description": { - "attribute": "Hangi valitud elemendi atribuudi v\u00e4\u00e4rtus", "authentication": "HTTP kasutaja tuvastamise meetod; algeline v\u00f5i muu", - "device_class": "Kasutajaliidesesse lisatava anduri ikooni t\u00fc\u00fcp/klass", "headers": "Veebip\u00e4ringus kasutatav p\u00e4is", - "index": "M\u00e4\u00e4rab milline element tagastatakse kasutatava CSS valiku alusel", "resource": "Veebilehe URL ei sisalda soovitud v\u00e4\u00e4rtusi", - "select": "M\u00e4\u00e4rab otsitava v\u00f5tmes\u00f5na. Vaata Beatifulsoup CSS valimeid", - "state_class": "Anduri olekuklass", - "value_template": "M\u00e4\u00e4rab anduri oleku saamiseks vajaliku malli", + "timeout": "Veebilehe ajal\u00f5pp", "verify_ssl": "Lubab v\u00f5i keelab SSL/TLS serdi tuvastamise n\u00e4iteks juhul kui sert on ise allkirjastatud" + }, + "menu_options": { + "add_sensor": "Lisa andur", + "remove_sensor": "Eemalda andur", + "resource": "Seadista resurss" + } + }, + "resource": { + "data": { + "authentication": "Vali tuvastusmeetod", + "headers": "P\u00e4ised", + "method": "Meetod", + "password": "", + "resource": "Resurss", + "timeout": "Ajal\u00f5pp", + "username": "", + "verify_ssl": "" + }, + "data_description": { + "authentication": "HTTP tuvastusmeetod- kas tavaline v\u00f5i k\u00fcsitlus", + "headers": "Veebip\u00e4ringus kasutatavad p\u00e4ised", + "resource": "V\u00e4\u00e4rtust sisaldava veebilehe aadress", + "timeout": "Veebi\u00fchenduse ajal\u00f5pp", + "verify_ssl": "Keelab/lubab SSL/TLS serdi kontrolli n\u00e4iteks ise loodud sert" } } } diff --git a/homeassistant/components/scrape/translations/fr.json b/homeassistant/components/scrape/translations/fr.json index f68ce5808b7..6e4c0f45b67 100644 --- a/homeassistant/components/scrape/translations/fr.json +++ b/homeassistant/components/scrape/translations/fr.json @@ -4,33 +4,34 @@ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "step": { - "user": { + "sensor": { "data": { "attribute": "Attribut", - "authentication": "Authentification", - "device_class": "Classe d'appareil", - "headers": "En-t\u00eates", "index": "Index", "name": "Nom", + "state_class": "Classe d'\u00e9tat", + "unit_of_measurement": "Unit\u00e9 de mesure" + }, + "data_description": { + "state_class": "La state_class du capteur" + } + }, + "user": { + "data": { + "authentication": "S\u00e9lectionner la m\u00e9thode d\u2019authentification", + "headers": "En-t\u00eates", + "method": "M\u00e9thode", "password": "Mot de passe", "resource": "Ressource", - "select": "S\u00e9lectionner", - "state_class": "Classe d'\u00e9tat", - "unit_of_measurement": "Unit\u00e9 de mesure", + "timeout": "D\u00e9lai d\u2019expiration", "username": "Nom d'utilisateur", - "value_template": "Mod\u00e8le de valeur", "verify_ssl": "V\u00e9rifier le certificat SSL" }, "data_description": { - "attribute": "Obtenir la valeur d'un attribut de la balise s\u00e9lectionn\u00e9e", "authentication": "M\u00e9thode d'authentification HTTP. \u00ab\u00a0basic\u00a0\u00bb ou \u00ab\u00a0digest\u00a0\u00bb", - "device_class": "Le type (la classe) du capteur qui d\u00e9finira l'ic\u00f4ne dans l'interface", "headers": "Les en-t\u00eates \u00e0 utiliser pour la requ\u00eate Web", - "index": "D\u00e9finit l'\u00e9l\u00e9ment \u00e0 utiliser parmi ceux renvoy\u00e9s par le s\u00e9lecteur CSS", "resource": "L'URL du site web qui contient la valeur", - "select": "D\u00e9finit la balise \u00e0 rechercher. Consultez les s\u00e9lecteurs CSS de Beautifulsoup pour plus de d\u00e9tails", - "state_class": "La state_class du capteur", - "value_template": "D\u00e9finit un mod\u00e8le pour obtenir l'\u00e9tat du capteur", + "timeout": "D\u00e9lai d\u2019expiration pour la connexion au site Web", "verify_ssl": "Active ou d\u00e9sactive la v\u00e9rification du certificat SSL/TLS, par exemple s'il est auto-sign\u00e9" } } @@ -40,31 +41,20 @@ "step": { "init": { "data": { - "attribute": "Attribut", - "authentication": "Authentification", - "device_class": "Classe d'appareil", + "authentication": "S\u00e9lectionner la m\u00e9thode d\u2019authentification", "headers": "En-t\u00eates", - "index": "Index", - "name": "Nom", + "method": "M\u00e9thode", "password": "Mot de passe", "resource": "Ressource", - "select": "S\u00e9lectionner", - "state_class": "Classe d'\u00e9tat", - "unit_of_measurement": "Unit\u00e9 de mesure", + "timeout": "D\u00e9lai d\u2019expiration", "username": "Nom d'utilisateur", - "value_template": "Mod\u00e8le de valeur", "verify_ssl": "V\u00e9rifier le certificat SSL" }, "data_description": { - "attribute": "Obtenir la valeur d'un attribut de la balise s\u00e9lectionn\u00e9e", "authentication": "M\u00e9thode d'authentification HTTP. \u00ab\u00a0basic\u00a0\u00bb ou \u00ab\u00a0digest\u00a0\u00bb", - "device_class": "Le type (la classe) du capteur qui d\u00e9finira l'ic\u00f4ne dans l'interface", "headers": "Les en-t\u00eates \u00e0 utiliser pour la requ\u00eate Web", - "index": "D\u00e9finit l'\u00e9l\u00e9ment \u00e0 utiliser parmi ceux renvoy\u00e9s par le s\u00e9lecteur CSS", "resource": "L'URL du site web qui contient la valeur", - "select": "D\u00e9finit la balise \u00e0 rechercher. Consultez les s\u00e9lecteurs CSS de Beautifulsoup pour plus de d\u00e9tails", - "state_class": "La state_class du capteur", - "value_template": "D\u00e9finit un mod\u00e8le pour obtenir l'\u00e9tat du capteur", + "timeout": "D\u00e9lai d\u2019expiration pour la connexion au site Web", "verify_ssl": "Active ou d\u00e9sactive la v\u00e9rification du certificat SSL/TLS, par exemple s'il est auto-sign\u00e9" } } diff --git a/homeassistant/components/scrape/translations/he.json b/homeassistant/components/scrape/translations/he.json index 6dd1e6845de..144a9567d17 100644 --- a/homeassistant/components/scrape/translations/he.json +++ b/homeassistant/components/scrape/translations/he.json @@ -6,13 +6,9 @@ "step": { "user": { "data": { - "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" - }, - "data_description": { - "state_class": "\u05d4-state_class \u05e9\u05dc \u05d4\u05d7\u05d9\u05d9\u05e9\u05df" } } } @@ -21,13 +17,9 @@ "step": { "init": { "data": { - "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" - }, - "data_description": { - "state_class": "\u05d4-state_class \u05e9\u05dc \u05d4\u05d7\u05d9\u05d9\u05e9\u05df" } } } diff --git a/homeassistant/components/scrape/translations/hu.json b/homeassistant/components/scrape/translations/hu.json index 7af59751b98..1bf1f159154 100644 --- a/homeassistant/components/scrape/translations/hu.json +++ b/homeassistant/components/scrape/translations/hu.json @@ -6,65 +6,43 @@ "step": { "user": { "data": { - "attribute": "Attrib\u00fatum", "authentication": "Hiteles\u00edt\u00e9s", - "device_class": "Eszk\u00f6zoszt\u00e1ly", "headers": "Fejl\u00e9cek", - "index": "Index", - "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "resource": "Er\u0151forr\u00e1s", - "select": "Kiv\u00e1laszt\u00e1s", - "state_class": "\u00c1llapotoszt\u00e1ly", - "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", - "value_template": "\u00c9rt\u00e9ksablon", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, "data_description": { - "attribute": "Egy attrib\u00fatum \u00e9rt\u00e9k\u00e9nek lek\u00e9r\u00e9se a kiv\u00e1lasztott c\u00edmk\u00e9n", "authentication": "A HTTP-hiteles\u00edt\u00e9s t\u00edpusa. Basic vagy digest", - "device_class": "Az \u00e9rz\u00e9kel\u0151 t\u00edpusa/oszt\u00e1lya az ikonnak a kezl\u0151fel\u00fcleten val\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1hoz", "headers": "A webes k\u00e9r\u00e9shez haszn\u00e1land\u00f3 fejl\u00e9cek", - "index": "Meghat\u00e1rozza, hogy a CSS-v\u00e1laszt\u00f3 \u00e1ltal visszaadott elemek k\u00f6z\u00fcl melyiket haszn\u00e1lja.", "resource": "Az \u00e9rt\u00e9ket tartalmaz\u00f3 weboldal URL c\u00edme", - "select": "Meghat\u00e1rozza, hogy milyen c\u00edmk\u00e9t keressen. N\u00e9zze meg a Beautifulsoup CSS szelektorokat a r\u00e9szletek\u00e9rt", - "state_class": "Az \u00e9rz\u00e9kel\u0151 \u00e1llapot oszt\u00e1lya", - "value_template": "Meghat\u00e1roz egy sablont az \u00e9rz\u00e9kel\u0151 \u00e1llapot\u00e1nak lek\u00e9rdez\u00e9s\u00e9re.", "verify_ssl": "Az SSL/TLS tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9s\u00e9nek enged\u00e9lyez\u00e9se/letilt\u00e1sa, p\u00e9ld\u00e1ul ha saj\u00e1t al\u00e1\u00edr\u00e1s\u00fa." } } } }, + "issues": { + "moved_yaml": { + "description": "A Scrape konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val \u00e1tker\u00fclt az integr\u00e1ci\u00f3ba.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 m\u00e9g 2 verzi\u00f3n kereszt\u00fcl fog m\u0171k\u00f6dni.\n\nA dokument\u00e1ci\u00f3nak megfelel\u0151en migr\u00e1lja a YAML konfigur\u00e1ci\u00f3j\u00e1t az integr\u00e1ci\u00f3s kulcsra.", + "title": "A Scrape YAML konfigur\u00e1ci\u00f3 \u00e1thelyez\u00e9sre ker\u00fclt" + } + }, "options": { "step": { "init": { "data": { - "attribute": "Attrib\u00fatum", "authentication": "Hiteles\u00edt\u00e9s", - "device_class": "Eszk\u00f6zoszt\u00e1ly", "headers": "Fejl\u00e9cek", - "index": "Index", - "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "resource": "Er\u0151forr\u00e1s", - "select": "Kiv\u00e1laszt\u00e1s", - "state_class": "\u00c1llapotoszt\u00e1ly", - "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", - "value_template": "\u00c9rt\u00e9ksablon", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, "data_description": { - "attribute": "Egy attrib\u00fatum \u00e9rt\u00e9k\u00e9nek lek\u00e9r\u00e9se a kiv\u00e1lasztott c\u00edmk\u00e9n", "authentication": "A HTTP-hiteles\u00edt\u00e9s t\u00edpusa. Basic vagy digest", - "device_class": "Az \u00e9rz\u00e9kel\u0151 t\u00edpusa/oszt\u00e1lya az ikonnak a kezl\u0151fel\u00fcleten val\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1hoz", "headers": "A webes k\u00e9r\u00e9shez haszn\u00e1land\u00f3 fejl\u00e9cek", - "index": "Meghat\u00e1rozza, hogy a CSS-v\u00e1laszt\u00f3 \u00e1ltal visszaadott elemek k\u00f6z\u00fcl melyiket haszn\u00e1lja.", "resource": "Az \u00e9rt\u00e9ket tartalmaz\u00f3 weboldal URL c\u00edme", - "select": "Meghat\u00e1rozza, hogy milyen c\u00edmk\u00e9t keressen. N\u00e9zze meg a Beautifulsoup CSS szelektorokat a r\u00e9szletek\u00e9rt", - "state_class": "Az \u00e9rz\u00e9kel\u0151 \u00e1llapot oszt\u00e1lya", - "value_template": "Meghat\u00e1roz egy sablont az \u00e9rz\u00e9kel\u0151 \u00e1llapot\u00e1nak lek\u00e9rdez\u00e9s\u00e9re.", "verify_ssl": "Az SSL/TLS tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9s\u00e9nek enged\u00e9lyez\u00e9se/letilt\u00e1sa, p\u00e9ld\u00e1ul ha saj\u00e1t al\u00e1\u00edr\u00e1s\u00fa." } } diff --git a/homeassistant/components/scrape/translations/id.json b/homeassistant/components/scrape/translations/id.json index e7761f73a1f..f0b017d651e 100644 --- a/homeassistant/components/scrape/translations/id.json +++ b/homeassistant/components/scrape/translations/id.json @@ -4,67 +4,117 @@ "already_configured": "Akun sudah dikonfigurasi" }, "step": { - "user": { + "sensor": { "data": { "attribute": "Atribut", - "authentication": "Autentikasi", "device_class": "Kelas Perangkat", - "headers": "Header", "index": "Indeks", "name": "Nama", - "password": "Kata Sandi", - "resource": "Sumber Daya", - "select": "Pilihan", + "select": "Pilih", "state_class": "Kelas Status", "unit_of_measurement": "Satuan Pengukuran", - "username": "Nama Pengguna", - "value_template": "Nilai Templat", - "verify_ssl": "Verifikasi sertifikat SSL" + "value_template": "Nilai Templat" }, "data_description": { "attribute": "Dapatkan nilai atribut pada tag yang dipilih", - "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", "device_class": "Jenis/kelas sensor untuk mengatur ikon di antarmuka", - "headers": "Header yang digunakan untuk permintaan web", "index": "Menentukan elemen mana yang dikembalikan oleh selektor CSS untuk digunakan", - "resource": "URL ke situs web yang mengandung nilai", "select": "Menentukan tag yang harus dicari. Periksa selektor CSS Beautifulsoup untuk melihat detailnya", "state_class": "Nilai state_class dari sensor", - "value_template": "Mendefinisikan templat untuk mendapatkan status sensor", + "unit_of_measurement": "Pilih pengukuran suhu atau buat sendiri", + "value_template": "Mendefinisikan templat untuk mendapatkan status sensor" + } + }, + "user": { + "data": { + "authentication": "Pilih metode autentikasi", + "headers": "Header", + "method": "Metode", + "password": "Kata Sandi", + "resource": "Sumber Daya", + "timeout": "Tenggang waktu", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "data_description": { + "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", + "headers": "Header yang digunakan untuk permintaan web", + "resource": "URL ke situs web yang mengandung nilai", + "timeout": "Tenggang waktu untuk koneksi ke situs web", "verify_ssl": "Mengaktifkan/menonaktifkan verifikasi sertifikat SSL/TLS, misalnya jika sertifikat ditandatangani sendiri" } } } }, + "issues": { + "moved_yaml": { + "description": "Konfigurasi Integrasi Scrape menggunakan YAML telah dipindahkan ke kunci integrasi.\n\nKonfigurasi YAML Anda yang ada saat ini akan berfungsi hingga 2 versi berikutnya.\n\nMigrasikan konfigurasi YAML Anda ke kunci integrasi sesuai dengan dokumentasi.", + "title": "Konfigurasi YAML Integrasi Scrape telah dihapus" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "Atribut", - "authentication": "Autentikasi", "device_class": "Kelas Perangkat", - "headers": "Header", "index": "Indeks", "name": "Nama", - "password": "Kata Sandi", - "resource": "Sumber Daya", - "select": "Pilihan", + "select": "Pilih", "state_class": "Kelas Status", "unit_of_measurement": "Satuan Pengukuran", - "username": "Nama Pengguna", - "value_template": "Nilai Templat", - "verify_ssl": "Verifikasi sertifikat SSL" + "value_template": "Nilai Templat" }, "data_description": { "attribute": "Dapatkan nilai atribut pada tag yang dipilih", - "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", "device_class": "Jenis/kelas sensor untuk mengatur ikon di antarmuka", - "headers": "Header yang digunakan untuk permintaan web", "index": "Menentukan elemen mana yang dikembalikan oleh selektor CSS untuk digunakan", - "resource": "URL ke situs web yang mengandung nilai", "select": "Menentukan tag yang harus dicari. Periksa selektor CSS Beautifulsoup untuk melihat detailnya", "state_class": "Nilai state_class dari sensor", - "value_template": "Mendefinisikan templat untuk mendapatkan status sensor", + "unit_of_measurement": "Pilih pengukuran suhu atau buat sendiri", + "value_template": "Mendefinisikan templat untuk mendapatkan status sensor" + } + }, + "init": { + "data": { + "authentication": "Pilih metode autentikasi", + "headers": "Header", + "method": "Metode", + "password": "Kata Sandi", + "resource": "Sumber Daya", + "timeout": "Tenggang waktu", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "data_description": { + "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", + "headers": "Header yang digunakan untuk permintaan web", + "resource": "URL ke situs web yang mengandung nilai", + "timeout": "Tenggang waktu untuk koneksi ke situs web", + "verify_ssl": "Mengaktifkan/menonaktifkan verifikasi sertifikat SSL/TLS, misalnya jika sertifikat ditandatangani sendiri" + }, + "menu_options": { + "add_sensor": "Tambahkan sensor", + "remove_sensor": "Hapus sensor", + "resource": "Konfigurasikan sumber daya" + } + }, + "resource": { + "data": { + "authentication": "Pilih metode autentikasi", + "headers": "Header", + "method": "Metode", + "password": "Kata Sandi", + "resource": "Sumber Daya", + "timeout": "Tenggang waktu", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "data_description": { + "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", + "headers": "Header yang digunakan untuk permintaan web", + "resource": "URL ke situs web yang mengandung nilai", + "timeout": "Tenggang waktu untuk koneksi ke situs web", "verify_ssl": "Mengaktifkan/menonaktifkan verifikasi sertifikat SSL/TLS, misalnya jika sertifikat ditandatangani sendiri" } } diff --git a/homeassistant/components/scrape/translations/it.json b/homeassistant/components/scrape/translations/it.json index e64ee3022d8..6367fd70613 100644 --- a/homeassistant/components/scrape/translations/it.json +++ b/homeassistant/components/scrape/translations/it.json @@ -3,68 +3,76 @@ "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, + "error": { + "resource_error": "Impossibile aggiornare i dati rest. Verificare la configurazione" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Attributo", - "authentication": "Autenticazione", "device_class": "Classe del dispositivo", - "headers": "Intestazioni", "index": "Indice", "name": "Nome", + "select": "Seleziona", + "state_class": "Classe di stato", + "unit_of_measurement": "Unit\u00e0 di misura", + "value_template": "Modello di valore" + }, + "data_description": { + "attribute": "Ottenere il valore di un attributo dell'etichetta selezionata", + "device_class": "Il tipo/classe del sensore per impostare l'icona nel frontend", + "index": "Definisce quale degli elementi restituiti dal selettore CSS utilizzare", + "select": "Definisce quale etichetta cercare. Controlla i selettori CSS di Beautifulsoup per i dettagli", + "state_class": "La state_class del sensore", + "unit_of_measurement": "Scegli l'unit\u00e0 di misura della temperatura o crearne una tua", + "value_template": "Definisce un modello per ottenere lo stato del sensore" + } + }, + "user": { + "data": { + "authentication": "Autenticazione", + "headers": "Intestazioni", + "method": "Metodo", "password": "Password", "resource": "Risorsa", - "select": "Seleziona", - "state_class": "Classe di Stato", - "unit_of_measurement": "Unit\u00e0 di misura", + "timeout": "Tempo scaduto", "username": "Nome utente", - "value_template": "Modello di valore", "verify_ssl": "Verifica il certificato SSL" }, "data_description": { - "attribute": "Ottieni il valore di un attributo sull'etichetta selezionata", "authentication": "Tipo di autenticazione HTTP. basic o digest", - "device_class": "Il tipo/classe del sensore per impostare l'icona nel frontend", "headers": "Intestazioni da utilizzare per la richiesta web", - "index": "Definisce quale degli elementi restituiti dal selettore CSS utilizzare", "resource": "L'URL del sito Web che contiene il valore", - "select": "Definisce quale etichetta cercare. Controlla i selettori CSS di Beautifulsoup per i dettagli", - "state_class": "La state_class del sensore", - "value_template": "Definisce un modello per ottenere lo stato del sensore", + "timeout": "Tiempo scaduto per la connessione al sito web", "verify_ssl": "Abilita/disabilita la verifica del certificato SSL/TLS, ad esempio se \u00e8 autofirmato" } } } }, + "issues": { + "moved_yaml": { + "description": "La configurazione di Scrape tramite YAML \u00e8 stata spostata nella chiave di integrazione. \n\nLa tua configurazione YAML esistente funzioner\u00e0 per altre 2 versioni. \n\nMigra la tua configurazione YAML alla chiave di integrazione in base alla documentazione.", + "title": "La configurazione YAML di Scrape \u00e8 stata spostata" + } + }, "options": { "step": { "init": { "data": { - "attribute": "Attributo", "authentication": "Autenticazione", - "device_class": "Classe del dispositivo", "headers": "Intestazioni", - "index": "Indice", - "name": "Nome", + "method": "Metodo", "password": "Password", "resource": "Risorsa", - "select": "Seleziona", - "state_class": "Classe di Stato", - "unit_of_measurement": "Unit\u00e0 di misura", + "timeout": "Tempo scaduto", "username": "Nome utente", - "value_template": "Modello di valore", "verify_ssl": "Verifica il certificato SSL" }, "data_description": { - "attribute": "Ottieni il valore di un attributo sull'etichetta selezionata", "authentication": "Tipo di autenticazione HTTP. basic o digest", - "device_class": "Il tipo/classe del sensore per impostare l'icona nel frontend", "headers": "Intestazioni da utilizzare per la richiesta web", - "index": "Definisce quale degli elementi restituiti dal selettore CSS utilizzare", "resource": "L'URL del sito Web che contiene il valore", - "select": "Definisce quale etichetta cercare. Controlla i selettori CSS di Beautifulsoup per i dettagli", - "state_class": "La state_class del sensore", - "value_template": "Definisce un modello per ottenere lo stato del sensore", + "timeout": "Tiempo scaduto per la connessione al sito web", "verify_ssl": "Abilita/disabilita la verifica del certificato SSL/TLS, ad esempio se \u00e8 autofirmato" } } diff --git a/homeassistant/components/scrape/translations/ja.json b/homeassistant/components/scrape/translations/ja.json index 554a9d2c37b..1caac0ac2ec 100644 --- a/homeassistant/components/scrape/translations/ja.json +++ b/homeassistant/components/scrape/translations/ja.json @@ -6,31 +6,17 @@ "step": { "user": { "data": { - "attribute": "\u5c5e\u6027", "authentication": "\u8a8d\u8a3c", - "device_class": "\u30c7\u30d0\u30a4\u30b9\u30af\u30e9\u30b9", "headers": "\u30d8\u30c3\u30c0\u30fc", - "index": "\u30a4\u30f3\u30c7\u30c3\u30af\u30b9", - "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "resource": "\u30ea\u30bd\u30fc\u30b9", - "select": "\u9078\u629e", - "state_class": "\u72b6\u614b\u30af\u30e9\u30b9(State Class)", - "unit_of_measurement": "\u6e2c\u5b9a\u306e\u5358\u4f4d", "username": "\u30e6\u30fc\u30b6\u30fc\u540d", - "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "data_description": { - "attribute": "\u9078\u629e\u3057\u305f\u30bf\u30b0\u306e\u5c5e\u6027\u306e\u5024\u3092\u53d6\u5f97\u3059\u308b", "authentication": "HTTP\u8a8d\u8a3c\u306e\u7a2e\u985e\u3002\u30d9\u30fc\u30b7\u30c3\u30af\u307e\u305f\u306f\u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u306e\u3069\u3061\u3089\u304b", - "device_class": "\u30d5\u30ed\u30f3\u30c8\u30a8\u30f3\u30c9\u306b\u30a2\u30a4\u30b3\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u306e\u30bf\u30a4\u30d7/\u30af\u30e9\u30b9", "headers": "Web\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u4f7f\u7528\u3059\u308b\u30d8\u30c3\u30c0\u30fc", - "index": "CSS\u30bb\u30ec\u30af\u30bf\u304c\u8fd4\u3059\u8981\u7d20\u306e\u3046\u3061\u3001\u3069\u306e\u8981\u7d20\u3092\u4f7f\u7528\u3059\u308b\u304b\u3092\u5b9a\u7fa9\u3057\u307e\u3059", "resource": "\u5024\u3092\u542b\u3080\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306eURL", - "select": "\u691c\u7d22\u3059\u308b\u30bf\u30b0\u3092\u5b9a\u7fa9\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Beautifulsoup CSS selectors\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", - "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)", - "value_template": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u3092\u53d6\u5f97\u3059\u308b\u305f\u3081\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u3092\u5b9a\u7fa9\u3057\u307e\u3059", "verify_ssl": "SSL/TLS\u8a3c\u660e\u66f8\u306e\u691c\u8a3c\u3092\u6709\u52b9/\u7121\u52b9\u306b\u3057\u307e\u3059\u3002(\u81ea\u5df1\u7f72\u540d\u306e\u5834\u5408\u306a\u3069)" } } @@ -40,31 +26,17 @@ "step": { "init": { "data": { - "attribute": "\u5c5e\u6027", "authentication": "\u8a8d\u8a3c", - "device_class": "\u30c7\u30d0\u30a4\u30b9\u30af\u30e9\u30b9", "headers": "\u30d8\u30c3\u30c0\u30fc", - "index": "\u30a4\u30f3\u30c7\u30c3\u30af\u30b9", - "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "resource": "\u30ea\u30bd\u30fc\u30b9", - "select": "\u9078\u629e", - "state_class": "\u72b6\u614b\u30af\u30e9\u30b9(State Class)", - "unit_of_measurement": "\u6e2c\u5b9a\u306e\u5358\u4f4d", "username": "\u30e6\u30fc\u30b6\u30fc\u540d", - "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "data_description": { - "attribute": "\u9078\u629e\u3057\u305f\u30bf\u30b0\u306e\u5c5e\u6027\u306e\u5024\u3092\u53d6\u5f97\u3059\u308b", "authentication": "HTTP\u8a8d\u8a3c\u306e\u7a2e\u985e\u3002\u30d9\u30fc\u30b7\u30c3\u30af\u307e\u305f\u306f\u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u306e\u3069\u3061\u3089\u304b", - "device_class": "\u30d5\u30ed\u30f3\u30c8\u30a8\u30f3\u30c9\u306b\u30a2\u30a4\u30b3\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u306e\u30bf\u30a4\u30d7/\u30af\u30e9\u30b9", "headers": "Web\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u4f7f\u7528\u3059\u308b\u30d8\u30c3\u30c0\u30fc", - "index": "CSS\u30bb\u30ec\u30af\u30bf\u304c\u8fd4\u3059\u8981\u7d20\u306e\u3046\u3061\u3001\u3069\u306e\u8981\u7d20\u3092\u4f7f\u7528\u3059\u308b\u304b\u3092\u5b9a\u7fa9\u3057\u307e\u3059", "resource": "\u5024\u3092\u542b\u3080\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306eURL", - "select": "\u691c\u7d22\u3059\u308b\u30bf\u30b0\u3092\u5b9a\u7fa9\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Beautifulsoup CSS selectors\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", - "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)", - "value_template": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u3092\u53d6\u5f97\u3059\u308b\u305f\u3081\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u3092\u5b9a\u7fa9\u3057\u307e\u3059", "verify_ssl": "SSL/TLS\u8a3c\u660e\u66f8\u306e\u691c\u8a3c\u3092\u6709\u52b9/\u7121\u52b9\u306b\u3057\u307e\u3059\u3002(\u81ea\u5df1\u7f72\u540d\u306e\u5834\u5408\u306a\u3069)" } } diff --git a/homeassistant/components/scrape/translations/nl.json b/homeassistant/components/scrape/translations/nl.json index 90e85d34677..dba74863b52 100644 --- a/homeassistant/components/scrape/translations/nl.json +++ b/homeassistant/components/scrape/translations/nl.json @@ -3,34 +3,47 @@ "abort": { "already_configured": "Account is al geconfigureerd" }, + "error": { + "resource_error": "Kon de REST data niet bijwerken, controleer je configuratie" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Attribuut", - "authentication": "Authenticatie", - "device_class": "Apparaatklasse", - "headers": "Headers", + "device_class": "Apparaatklasse (device_class)", "index": "Index", "name": "Naam", + "select": "Selecteer", + "state_class": "Statusklasse (state_class)", + "unit_of_measurement": "Eenheid", + "value_template": "Waardesjabloon (template)" + }, + "data_description": { + "attribute": "Bepaal de waarde van een attribuut op van de geselecteerde tag", + "device_class": "Type/klasse van de sensor voor het icoon in de frontend", + "index": "Bepaalt welke van de elementen worden teruggegeven door de CSS selector om te gebruiken", + "select": "Bepaalt de tag om naar te zoeken. Zie Beautifulsoup CSS selectors voor meer details", + "state_class": "De state_class van de sensor", + "unit_of_measurement": "Kies een temperatuurmeting of cre\u00eber er zelf een", + "value_template": "Definieert een template om de status van de sensor te bepalen" + } + }, + "user": { + "data": { + "authentication": "Authenticatie", + "headers": "Headers", + "method": "Methode", "password": "Wachtwoord", "resource": "Bron", - "select": "Selecteer", - "state_class": "Staatklasse", - "unit_of_measurement": "Meeteenheid", + "timeout": "Wachttijd verstreken", "username": "Gebruikersnaam", - "value_template": "Waardetemplate", "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "data_description": { - "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", "authentication": "Type van de HTTP-authenticatie. Ofwel basic of digest", - "device_class": "Het type/klasse van de sensor om het pictogram in de frontend in te stellen", "headers": "Headers om te gebruiken voor het webverzoek", - "index": "Definieert welke van de door de CSS-selector geretourneerde elementen moeten worden gebruikt", "resource": "De URL naar de website die de waarde bevat", - "select": "Definieert naar welke tag moet worden gezocht. Controleer Beautifulsoup CSS-selectors voor details", - "state_class": "De state_class van de sensor", - "value_template": "Definieert een sjabloon om de status van de sensor te krijgen", + "timeout": "Wachttijd verstreken bij het maken van een verbinding naar de website", "verify_ssl": "Activeert/de-activeert verificatie van SSL/TLS certificaat, als voorbeeld of het is zelf-getekend" } } @@ -40,31 +53,20 @@ "step": { "init": { "data": { - "attribute": "Attribuut", "authentication": "Authenticatie", - "device_class": "Apparaatklasse", "headers": "Headers", - "index": "Index", - "name": "Naam", + "method": "Methode", "password": "Wachtwoord", "resource": "Bron", - "select": "Selecteer", - "state_class": "Staatklasse", - "unit_of_measurement": "Meeteenheid", + "timeout": "Wachttijd verstreken", "username": "Gebruikersnaam", - "value_template": "Waardetemplate", "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "data_description": { - "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", "authentication": "Type van de HTTP-authenticatie. Ofwel basic of digest", - "device_class": "Het type/klasse van de sensor om het pictogram in de frontend in te stellen", "headers": "Headers om te gebruiken voor het webverzoek", - "index": "Definieert welke van de door de CSS-selector geretourneerde elementen moeten worden gebruikt", "resource": "De URL naar de website die de waarde bevat", - "select": "Definieert naar welke tag moet worden gezocht. Controleer Beautifulsoup CSS-selectors voor details", - "state_class": "De state_class van de sensor", - "value_template": "Definieert een sjabloon om de status van de sensor te krijgen", + "timeout": "Wachttijd verstreken bij het maken van een verbinding naar de website", "verify_ssl": "Activeert/de-activeert verificatie van SSL/TLS certificaat, als voorbeeld of het is zelf-getekend" } } diff --git a/homeassistant/components/scrape/translations/no.json b/homeassistant/components/scrape/translations/no.json index 6738c8a630a..0050ead5208 100644 --- a/homeassistant/components/scrape/translations/no.json +++ b/homeassistant/components/scrape/translations/no.json @@ -3,68 +3,121 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert" }, + "error": { + "resource_error": "Kunne ikke oppdatere hviledata. Bekreft konfigurasjonen din" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Attributt", - "authentication": "Godkjenning", "device_class": "Enhetsklasse", - "headers": "Overskrifter", "index": "Indeks", "name": "Navn", - "password": "Passord", - "resource": "Ressurs", "select": "Velg", "state_class": "Statsklasse", "unit_of_measurement": "M\u00e5leenhet", - "username": "Brukernavn", - "value_template": "Verdimal", - "verify_ssl": "Verifisere SSL-sertifikat" + "value_template": "Verdimal" }, "data_description": { "attribute": "F\u00e5 verdien av et attributt p\u00e5 den valgte taggen", - "authentication": "Type HTTP-godkjenning. Enten grunnleggende eller ufullstendig", - "device_class": "Typen/klassen til sensoren for \u00e5 angi ikonet i frontend", - "headers": "Overskrifter som skal brukes for nettforesp\u00f8rselen", + "device_class": "Type/klasse av sensoren for \u00e5 angi ikonet i frontend", "index": "Definerer hvilke av elementene som returneres av CSS-velgeren som skal brukes", - "resource": "URL-en til nettstedet som inneholder verdien", "select": "Definerer hvilken tag som skal s\u00f8kes etter. Sjekk Beautifulsoup CSS-velgere for detaljer", "state_class": "Sensorens state_class", - "value_template": "Definerer en mal for \u00e5 f\u00e5 tilstanden til sensoren", + "unit_of_measurement": "Velg temperaturm\u00e5ling eller lag din egen", + "value_template": "Definerer en mal for \u00e5 f\u00e5 tilstanden til sensoren" + } + }, + "user": { + "data": { + "authentication": "Velg autentiseringsmetode", + "headers": "Overskrifter", + "method": "Metode", + "password": "Passord", + "resource": "Ressurs", + "timeout": "Tidsavbrudd", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "data_description": { + "authentication": "Type HTTP-godkjenning. Enten grunnleggende eller ufullstendig", + "headers": "Overskrifter som skal brukes for nettforesp\u00f8rselen", + "resource": "URL-en til nettstedet som inneholder verdien", + "timeout": "Tidsavbrudd for tilkobling til nettside", "verify_ssl": "Aktiverer/deaktiverer verifisering av SSL/TLS-sertifikat, for eksempel hvis det er selvsignert" } } } }, + "issues": { + "moved_yaml": { + "description": "Konfigurering av Scrape ved hjelp av YAML har blitt flyttet til integrasjonsn\u00f8kkel. \n\n Din eksisterende YAML-konfigurasjon vil fungere for 2 flere versjoner. \n\n Migrer YAML-konfigurasjonen til integrasjonsn\u00f8kkelen i henhold til dokumentasjonen.", + "title": "Scrape YAML-konfigurasjonen er flyttet" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "Attributt", - "authentication": "Godkjenning", "device_class": "Enhetsklasse", - "headers": "Overskrifter", "index": "Indeks", "name": "Navn", - "password": "Passord", - "resource": "Ressurs", "select": "Velg", "state_class": "Statsklasse", "unit_of_measurement": "M\u00e5leenhet", - "username": "Brukernavn", - "value_template": "Verdimal", - "verify_ssl": "Verifisere SSL-sertifikat" + "value_template": "Verdimal" }, "data_description": { "attribute": "F\u00e5 verdien av et attributt p\u00e5 den valgte taggen", - "authentication": "Type HTTP-godkjenning. Enten grunnleggende eller ufullstendig", - "device_class": "Typen/klassen til sensoren for \u00e5 angi ikonet i frontend", - "headers": "Overskrifter som skal brukes for nettforesp\u00f8rselen", + "device_class": "Type/klasse av sensoren for \u00e5 angi ikonet i frontend", "index": "Definerer hvilke av elementene som returneres av CSS-velgeren som skal brukes", - "resource": "URL-en til nettstedet som inneholder verdien", "select": "Definerer hvilken tag som skal s\u00f8kes etter. Sjekk Beautifulsoup CSS-velgere for detaljer", "state_class": "Sensorens state_class", - "value_template": "Definerer en mal for \u00e5 f\u00e5 tilstanden til sensoren", + "unit_of_measurement": "Velg temperaturm\u00e5ling eller lag din egen", + "value_template": "Definerer en mal for \u00e5 f\u00e5 tilstanden til sensoren" + } + }, + "init": { + "data": { + "authentication": "Velg autentiseringsmetode", + "headers": "Overskrifter", + "method": "Metode", + "password": "Passord", + "resource": "Ressurs", + "timeout": "Tidsavbrudd", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "data_description": { + "authentication": "Type HTTP-godkjenning. Enten grunnleggende eller ufullstendig", + "headers": "Overskrifter som skal brukes for nettforesp\u00f8rselen", + "resource": "URL-en til nettstedet som inneholder verdien", + "timeout": "Tidsavbrudd for tilkobling til nettside", + "verify_ssl": "Aktiverer/deaktiverer verifisering av SSL/TLS-sertifikat, for eksempel hvis det er selvsignert" + }, + "menu_options": { + "add_sensor": "Legg til sensor", + "remove_sensor": "Fjern sensoren", + "resource": "Konfigurer ressurs" + } + }, + "resource": { + "data": { + "authentication": "Velg autentiseringsmetode", + "headers": "Overskrifter", + "method": "Metode", + "password": "Passord", + "resource": "Ressurs", + "timeout": "Tidsavbrudd", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "data_description": { + "authentication": "Type HTTP-godkjenning. Enten grunnleggende eller ufullstendig", + "headers": "Overskrifter som skal brukes for nettforesp\u00f8rselen", + "resource": "URL-en til nettstedet som inneholder verdien", + "timeout": "Tidsavbrudd for tilkobling til nettside", "verify_ssl": "Aktiverer/deaktiverer verifisering av SSL/TLS-sertifikat, for eksempel hvis det er selvsignert" } } diff --git a/homeassistant/components/scrape/translations/pl.json b/homeassistant/components/scrape/translations/pl.json index 67b2a3db685..8e05d2e9474 100644 --- a/homeassistant/components/scrape/translations/pl.json +++ b/homeassistant/components/scrape/translations/pl.json @@ -3,68 +3,76 @@ "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane" }, + "error": { + "resource_error": "Nie mo\u017cna zaktualizowa\u0107 danych \"rest\". Sprawd\u017a swoj\u0105 konfiguracj\u0119." + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Atrybut", - "authentication": "Uwierzytelnianie", "device_class": "Klasa urz\u0105dzenia", - "headers": "Nag\u0142\u00f3wki", "index": "Indeks", "name": "Nazwa", - "password": "Has\u0142o", - "resource": "Zas\u00f3b", "select": "Wybierz", "state_class": "Klasa stanu", "unit_of_measurement": "Jednostka miary", - "username": "Nazwa u\u017cytkownika", - "value_template": "Szablon warto\u015bci", - "verify_ssl": "Weryfikacja certyfikatu SSL" + "value_template": "Szablon warto\u015bci" }, "data_description": { "attribute": "Pobierz warto\u015b\u0107 atrybutu w wybranym tagu", - "authentication": "Typ uwierzytelniania HTTP. Podstawowy lub digest.", "device_class": "Typ/klasa sensora do ustawienia ikony w interfejsie u\u017cytkownika", - "headers": "Nag\u0142\u00f3wki do u\u017cycia w \u017c\u0105daniu internetowym", "index": "Okre\u015bla, kt\u00f3rego z element\u00f3w zwracanych przez selektor CSS nale\u017cy u\u017cy\u0107", - "resource": "Adres URL strony internetowej zawieraj\u0105cej t\u0105 warto\u015b\u0107", "select": "Okre\u015bla jakiego taga szuka\u0107. Sprawd\u017a selektory CSS Beautifulsoup, aby uzyska\u0107 szczeg\u00f3\u0142owe informacje.", "state_class": "state_class sensora", - "value_template": "Szablon, kt\u00f3ry pozwala uzyska\u0107 stan czujnika", + "unit_of_measurement": "Wybierz pomiar temperatury lub stw\u00f3rz w\u0142asny", + "value_template": "Szablon, kt\u00f3ry pozwala uzyska\u0107 stan czujnika" + } + }, + "user": { + "data": { + "authentication": "Wybierz metod\u0119 uwierzytelniania", + "headers": "Nag\u0142\u00f3wki", + "method": "Metoda", + "password": "Has\u0142o", + "resource": "Zas\u00f3b", + "timeout": "Limit czasu", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "data_description": { + "authentication": "Typ uwierzytelniania HTTP. Podstawowy lub digest.", + "headers": "Nag\u0142\u00f3wki do u\u017cycia w \u017c\u0105daniu internetowym", + "resource": "Adres URL strony internetowej zawieraj\u0105cej t\u0105 warto\u015b\u0107", + "timeout": "Limit czasu na po\u0142\u0105czenie z witryn\u0105", "verify_ssl": "W\u0142\u0105cza/wy\u0142\u0105cza weryfikacj\u0119 certyfikatu SSL/TLS, na przyk\u0142ad, je\u015bli jest on samopodpisany." } } } }, + "issues": { + "moved_yaml": { + "description": "Konfiguracja Scrape za pomoc\u0105 YAML zosta\u0142a przeniesiona do klucza integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML b\u0119dzie dzia\u0142a\u0107 przez kolejne 2 wersje. \n\nPrzenie\u015b swoj\u0105 konfiguracj\u0119 YAML do klucza integracji zgodnie z dokumentacj\u0105.", + "title": "Konfiguracja YAML dla Scrape zostaje przeniesiona" + } + }, "options": { "step": { "init": { "data": { - "attribute": "Atrybut", - "authentication": "Uwierzytelnianie", - "device_class": "Klasa urz\u0105dzenia", + "authentication": "Wybierz metod\u0119 uwierzytelniania", "headers": "Nag\u0142\u00f3wki", - "index": "Indeks", - "name": "Nazwa", + "method": "Metoda", "password": "Has\u0142o", "resource": "Zas\u00f3b", - "select": "Wybierz", - "state_class": "Klasa stanu", - "unit_of_measurement": "Jednostka miary", + "timeout": "Limit czasu", "username": "Nazwa u\u017cytkownika", - "value_template": "Szablon warto\u015bci", "verify_ssl": "Weryfikacja certyfikatu SSL" }, "data_description": { - "attribute": "Pobierz warto\u015b\u0107 atrybutu w wybranym tagu", "authentication": "Typ uwierzytelniania HTTP. Podstawowy lub digest.", - "device_class": "Typ/klasa sensora do ustawienia ikony w interfejsie u\u017cytkownika", "headers": "Nag\u0142\u00f3wki do u\u017cycia w \u017c\u0105daniu internetowym", - "index": "Okre\u015bla, kt\u00f3rego z element\u00f3w zwracanych przez selektor CSS nale\u017cy u\u017cy\u0107", "resource": "Adres URL strony internetowej zawieraj\u0105cej t\u0105 warto\u015b\u0107", - "select": "Okre\u015bla jakiego taga szuka\u0107. Sprawd\u017a selektory CSS Beautifulsoup, aby uzyska\u0107 szczeg\u00f3\u0142owe informacje.", - "state_class": "state_class sensora", - "value_template": "Szablon, kt\u00f3ry pozwala uzyska\u0107 stan czujnika", + "timeout": "Limit czasu na po\u0142\u0105czenie z witryn\u0105", "verify_ssl": "W\u0142\u0105cza/wy\u0142\u0105cza weryfikacj\u0119 certyfikatu SSL/TLS, na przyk\u0142ad, je\u015bli jest on samopodpisany." } } diff --git a/homeassistant/components/scrape/translations/pt-BR.json b/homeassistant/components/scrape/translations/pt-BR.json index 84d7aaf6807..729aa589102 100644 --- a/homeassistant/components/scrape/translations/pt-BR.json +++ b/homeassistant/components/scrape/translations/pt-BR.json @@ -3,68 +3,117 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada" }, + "error": { + "resource_error": "N\u00e3o foi poss\u00edvel atualizar os dados de descanso. Verifique sua configura\u00e7\u00e3o" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "Atributo", - "authentication": "Autentica\u00e7\u00e3o", "device_class": "Classe do dispositivo", - "headers": "Cabe\u00e7alhos", "index": "\u00cdndice", "name": "Nome", + "select": "Selecione", + "state_class": "Classe do Estado", + "unit_of_measurement": "Unidade de medida", + "value_template": "Modelo de valor" + }, + "data_description": { + "attribute": "Obtenha o valor de um atributo na tag selecionada", + "device_class": "O tipo/classe do sensor para definir o \u00edcone no frontend", + "index": "Define qual dos elementos retornados pelo seletor CSS usar", + "select": "Define qual tag procurar. Verifique os seletores CSS do Beautifulsoup para obter detalhes", + "state_class": "O estado_classe do sensor", + "unit_of_measurement": "Escolha a medi\u00e7\u00e3o de temperatura ou crie a sua pr\u00f3pria", + "value_template": "Define um modelo para obter o estado do sensor" + } + }, + "user": { + "data": { + "authentication": "Selecione o m\u00e9todo de autentica\u00e7\u00e3o", + "headers": "Cabe\u00e7alhos", + "method": "M\u00e9todo", "password": "Senha", "resource": "Recurso", - "select": "Selecionar", - "state_class": "Classe de estado", - "unit_of_measurement": "Unidade de medida", + "timeout": "Tempo limite", "username": "Usu\u00e1rio", - "value_template": "Modelo de valor", "verify_ssl": "Verifique o certificado SSL" }, "data_description": { - "attribute": "Obter valor de um atributo na tag selecionada", "authentication": "Tipo de autentica\u00e7\u00e3o HTTP. b\u00e1sica ou digerida", - "device_class": "O tipo/classe do sensor para definir o \u00edcone na frontend", "headers": "Cabe\u00e7alhos a serem usados para a solicita\u00e7\u00e3o da web", - "index": "Define qual dos elementos retornados pelo seletor CSS usar", "resource": "A URL para o site que cont\u00e9m o valor", - "select": "Define qual tag pesquisar. Verifique os seletores CSS da Beautiful Soup para obter detalhes", - "state_class": "A classe de estado do sensor", - "value_template": "Define um modelo para obter o estado do sensor", + "timeout": "Tempo limite para conex\u00e3o com o site", "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" } } } }, + "issues": { + "moved_yaml": { + "description": "A configura\u00e7\u00e3o do Scrape usando YAML foi movida para a chave de integra\u00e7\u00e3o. \n\n Sua configura\u00e7\u00e3o YAML existente funcionar\u00e1 para mais duas vers\u00f5es. \n\n Migre sua configura\u00e7\u00e3o YAML para a chave de integra\u00e7\u00e3o de acordo com a documenta\u00e7\u00e3o.", + "title": "A configura\u00e7\u00e3o YAML de Scrape foi movida" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "Atributo", - "authentication": "Autentica\u00e7\u00e3o", - "device_class": "Classe do dispositivo", - "headers": "Cabe\u00e7alhos", + "device_class": "Classe de dispositivo", "index": "\u00cdndice", - "name": "Nome", + "select": "Selecione", + "state_class": "Classe do estado", + "unit_of_measurement": "Unidade de medida", + "value_template": "Modelo de valor" + }, + "data_description": { + "attribute": "Obtenha o valor de um atributo na tag selecionada", + "device_class": "O tipo/classe do sensor para definir o \u00edcone no frontend", + "index": "Define qual dos elementos retornados pelo seletor CSS usar", + "select": "Define qual tag procurar. Verifique os seletores CSS do Beautifulsoup para obter detalhes", + "state_class": "O estado_classe do sensor", + "unit_of_measurement": "Escolha a medi\u00e7\u00e3o de temperatura ou crie a sua pr\u00f3pria", + "value_template": "Define um modelo para obter o estado do sensor" + } + }, + "init": { + "data": { + "authentication": "Selecione o m\u00e9todo de autentica\u00e7\u00e3o", + "headers": "Cabe\u00e7alhos", + "method": "M\u00e9todo", "password": "Senha", "resource": "Recurso", - "select": "Selecionar", - "state_class": "Classe de estado", - "unit_of_measurement": "Unidade de medida", + "timeout": "Tempo limite", "username": "Usu\u00e1rio", - "value_template": "Modelo de valor", "verify_ssl": "Verificar SSL" }, "data_description": { - "attribute": "Obter valor de um atributo na tag selecionada", "authentication": "Tipo de autentica\u00e7\u00e3o HTTP. b\u00e1sica ou digerida", - "device_class": "O tipo/classe do sensor para definir o \u00edcone na frontend", "headers": "Cabe\u00e7alhos a serem usados para a solicita\u00e7\u00e3o da web", - "index": "Define qual dos elementos retornados pelo seletor CSS usar", "resource": "A URL para o site que cont\u00e9m o valor", - "select": "Define qual tag pesquisar. Verifique os seletores CSS da Beautiful Soup para obter detalhes", - "state_class": "A classe de estado do sensor", - "value_template": "Define um modelo para obter o estado do sensor", + "timeout": "Tempo limite para conex\u00e3o com o site", + "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" + }, + "menu_options": { + "add_sensor": "Adicionar sensor", + "remove_sensor": "Remover sensor", + "resource": "Configurar recurso" + } + }, + "resource": { + "data": { + "authentication": "Selecione o m\u00e9todo de autentica\u00e7\u00e3o", + "headers": "Cabe\u00e7alhos", + "method": "M\u00e9todo", + "resource": "Recurso", + "timeout": "Tempo esgotado" + }, + "data_description": { + "authentication": "Tipo da autentica\u00e7\u00e3o HTTP. Seja b\u00e1sico ou resumido", + "headers": "Cabe\u00e7alhos a serem usados para a solicita\u00e7\u00e3o da web", + "resource": "A URL para o site que cont\u00e9m o valor", + "timeout": "Tempo esgotado para conex\u00e3o com o site", "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" } } diff --git a/homeassistant/components/scrape/translations/pt.json b/homeassistant/components/scrape/translations/pt.json index 003da0eed66..3ad22ead218 100644 --- a/homeassistant/components/scrape/translations/pt.json +++ b/homeassistant/components/scrape/translations/pt.json @@ -2,23 +2,6 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada" - }, - "step": { - "user": { - "data": { - "name": "Nome", - "unit_of_measurement": "Unidade de Medida" - } - } - } - }, - "options": { - "step": { - "init": { - "data": { - "unit_of_measurement": "Unidade de Medida" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/ru.json b/homeassistant/components/scrape/translations/ru.json index 2d014592e85..e212e487f98 100644 --- a/homeassistant/components/scrape/translations/ru.json +++ b/homeassistant/components/scrape/translations/ru.json @@ -3,34 +3,47 @@ "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": { + "resource_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, "step": { - "user": { + "sensor": { "data": { "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", - "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", - "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", - "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" }, "data_description": { "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", - "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", - "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", - "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", - "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "unit_of_measurement": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0438\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0432\u043e\u0435 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430." + } + }, + "user": { + "data": { + "authentication": "\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", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "method": "\u041c\u0435\u0442\u043e\u0434", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0441\u0430\u0439\u0442\u0443", "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." } } @@ -38,33 +51,67 @@ }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", - "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", - "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", - "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" }, "data_description": { "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", - "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", - "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", - "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", - "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "unit_of_measurement": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0438\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0432\u043e\u0435 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430." + } + }, + "init": { + "data": { + "authentication": "\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", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "method": "\u041c\u0435\u0442\u043e\u0434", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0441\u0430\u0439\u0442\u0443", + "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." + }, + "menu_options": { + "add_sensor": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440", + "remove_sensor": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440", + "resource": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441" + } + }, + "resource": { + "data": { + "authentication": "\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", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "method": "\u041c\u0435\u0442\u043e\u0434", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0441\u0430\u0439\u0442\u0443", "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." } } diff --git a/homeassistant/components/scrape/translations/sk.json b/homeassistant/components/scrape/translations/sk.json new file mode 100644 index 00000000000..f29805634e1 --- /dev/null +++ b/homeassistant/components/scrape/translations/sk.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "resource_error": "Nepodarilo sa aktualizova\u0165 ostatn\u00e9 \u00fadaje. Overte svoju konfigur\u00e1ciu" + }, + "step": { + "sensor": { + "data": { + "attribute": "Atrib\u00fat", + "name": "N\u00e1zov", + "select": "Vyberte", + "unit_of_measurement": "Jednotka merania", + "value_template": "\u0160abl\u00f3na hodnoty" + }, + "data_description": { + "unit_of_measurement": "Vyberte si meranie teploty alebo si vytvorte vlastn\u00e9" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + }, + "data_description": { + "timeout": "\u010casov\u00fd limit pre pripojenie k webovej str\u00e1nke" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + }, + "data_description": { + "timeout": "\u010casov\u00fd limit pre pripojenie k webovej str\u00e1nke" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index 130d7505018..96ecf2a934e 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -6,31 +6,17 @@ "step": { "user": { "data": { - "attribute": "Attribut", "authentication": "Autentisering", - "device_class": "Enhetsklass", "headers": "Headers", - "index": "Index", - "name": "Namn", "password": "L\u00f6senord", "resource": "Resurs", - "select": "V\u00e4lj", - "state_class": "Tillst\u00e5ndsklass", - "unit_of_measurement": "M\u00e5ttenhet", "username": "Anv\u00e4ndarnamn", - "value_template": "V\u00e4rdemall", "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { - "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen", "authentication": "Typ av HTTP-autentisering. Antingen basic eller digest", - "device_class": "Typ/klass av sensorn f\u00f6r att st\u00e4lla in ikonen i frontend", "headers": "Rubriker att anv\u00e4nda f\u00f6r webbf\u00f6rfr\u00e5gan", - "index": "Definierar vilka av elementen som returneras av CSS-v\u00e4ljaren som ska anv\u00e4ndas", "resource": "Webbadressen till webbplatsen som inneh\u00e5ller v\u00e4rdet", - "select": "Definierar vilken tagg som ska s\u00f6kas efter. Se Beautifulsoup CSS-selektorer f\u00f6r mer information.", - "state_class": "Tillst\u00e5ndsklassen f\u00f6r sensorn", - "value_template": "Definierar en mall f\u00f6r att f\u00e5 sensorns tillst\u00e5nd", "verify_ssl": "Aktiverar/inaktiverar verifiering av SSL/TLS-certifikat, till exempel om det \u00e4r sj\u00e4lvsignerat" } } @@ -40,31 +26,17 @@ "step": { "init": { "data": { - "attribute": "Attribut", "authentication": "Autentisering", - "device_class": "Enhetsklass", "headers": "Headers", - "index": "Index", - "name": "Namn", "password": "L\u00f6senord", "resource": "Resurs", - "select": "V\u00e4lj", - "state_class": "Tillst\u00e5ndsklass", - "unit_of_measurement": "M\u00e5ttenhet", "username": "Anv\u00e4ndarnamn", - "value_template": "V\u00e4rdemall", "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { - "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen", "authentication": "Typ av HTTP-autentisering. Antingen basic eller digest", - "device_class": "Typ/klass av sensorn f\u00f6r att st\u00e4lla in ikonen i frontend", "headers": "Rubriker att anv\u00e4nda f\u00f6r webbf\u00f6rfr\u00e5gan", - "index": "Definierar vilka av elementen som returneras av CSS-v\u00e4ljaren som ska anv\u00e4ndas", "resource": "Webbadressen till webbplatsen som inneh\u00e5ller v\u00e4rdet", - "select": "Definierar vilken tagg som ska s\u00f6kas efter. Se Beautifulsoup CSS-selektorer f\u00f6r mer information.", - "state_class": "Tillst\u00e5ndsklassen f\u00f6r sensorn", - "value_template": "Definierar en mall f\u00f6r att f\u00e5 sensorns tillst\u00e5nd", "verify_ssl": "Aktiverar/inaktiverar verifiering av SSL/TLS-certifikat, till exempel om det \u00e4r sj\u00e4lvsignerat" } } diff --git a/homeassistant/components/scrape/translations/tr.json b/homeassistant/components/scrape/translations/tr.json index 954ce1ad052..1bc92367580 100644 --- a/homeassistant/components/scrape/translations/tr.json +++ b/homeassistant/components/scrape/translations/tr.json @@ -6,31 +6,17 @@ "step": { "user": { "data": { - "attribute": "\u00d6znitelik", "authentication": "Kimlik do\u011frulama", - "device_class": "Cihaz S\u0131n\u0131f\u0131", "headers": "Ba\u015fl\u0131klar", - "index": "Dizin", - "name": "Ad", "password": "Parola", "resource": "Kaynak", - "select": "Se\u00e7", - "state_class": "Durum S\u0131n\u0131f\u0131", - "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", "username": "Kullan\u0131c\u0131 Ad\u0131", - "value_template": "De\u011fer \u015eablonu", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, "data_description": { - "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", - "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", - "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", - "select": "Hangi etiketin aranaca\u011f\u0131n\u0131 tan\u0131mlar. Ayr\u0131nt\u0131lar i\u00e7in Beautifulsoup CSS se\u00e7icilerini kontrol edin", - "state_class": "Sens\u00f6r\u00fcn state_class", - "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar", "verify_ssl": "\u00d6rne\u011fin, kendinden imzal\u0131ysa, SSL/TLS sertifikas\u0131n\u0131n do\u011frulanmas\u0131n\u0131 etkinle\u015ftirir/devre d\u0131\u015f\u0131 b\u0131rak\u0131r" } } @@ -40,31 +26,17 @@ "step": { "init": { "data": { - "attribute": "\u00d6znitelik", "authentication": "Kimlik do\u011frulama", - "device_class": "Cihaz S\u0131n\u0131f\u0131", "headers": "Ba\u015fl\u0131klar", - "index": "Dizin", - "name": "Ad", "password": "Parola", "resource": "Kaynak", - "select": "Se\u00e7", - "state_class": "Durum S\u0131n\u0131f\u0131", - "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", "username": "Kullan\u0131c\u0131 Ad\u0131", - "value_template": "De\u011fer \u015eablonu", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, "data_description": { - "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", - "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", - "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", - "select": "Hangi etiketin aranaca\u011f\u0131n\u0131 tan\u0131mlar. Ayr\u0131nt\u0131lar i\u00e7in Beautifulsoup CSS se\u00e7icilerini kontrol edin", - "state_class": "Sens\u00f6r\u00fcn state_class", - "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar", "verify_ssl": "\u00d6rne\u011fin, kendinden imzal\u0131ysa, SSL/TLS sertifikas\u0131n\u0131n do\u011frulanmas\u0131n\u0131 etkinle\u015ftirir/devre d\u0131\u015f\u0131 b\u0131rak\u0131r" } } diff --git a/homeassistant/components/scrape/translations/zh-Hans.json b/homeassistant/components/scrape/translations/zh-Hans.json new file mode 100644 index 00000000000..c178e103c2e --- /dev/null +++ b/homeassistant/components/scrape/translations/zh-Hans.json @@ -0,0 +1,39 @@ +{ + "config": { + "step": { + "sensor": { + "data_description": { + "attribute": "\u83b7\u53d6\u6240\u9009\u6807\u7b7e\u4e0a\u4e00\u4e2a\u5c5e\u6027\u7684\u503c", + "device_class": "\u4f20\u611f\u5668\u7c7b\u522b\uff0c\u4ee5\u4fbf\u5728\u524d\u7aef\u754c\u9762\u8bbe\u7f6e\u56fe\u6807", + "index": "\u5b9a\u4e49\u8981\u4f7f\u7528\u7684\u5143\u7d20\uff08 \u7531CSS \u9009\u62e9\u5668\u8fd4\u56de\uff09", + "select": "\u5b9a\u4e49\u8981\u641c\u7d22\u7684\u6807\u8bb0\uff08tag\uff09\u3002\u67e5\u770bBeautifulsoup CSS \u9009\u62e9\u5668\u4e86\u89e3\u8be6\u7ec6\u4fe1\u606f", + "state_class": "\u4f20\u611f\u5668\u7684state_class", + "unit_of_measurement": "\u9009\u62e9\u6216\u521b\u5efa\u6e29\u5ea6\u6d4b\u91cf\u5355\u4f4d", + "value_template": "\u5b9a\u4e49\u7528\u4e8e\u83b7\u53d6\u4f20\u611f\u5668\u72b6\u6001\u7684\u6a21\u677f" + } + }, + "user": { + "data": { + "method": "\u8bf7\u6c42\u65b9\u5f0f", + "timeout": "\u8d85\u65f6" + }, + "data_description": { + "timeout": "\u8fde\u63a5\u5230\u7f51\u7ad9\u8d85\u65f6" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "method": "\u8bf7\u6c42\u65b9\u5f0f", + "timeout": "\u8d85\u65f6" + }, + "data_description": { + "timeout": "\u8fde\u63a5\u5230\u7f51\u7ad9\u8d85\u65f6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/zh-Hant.json b/homeassistant/components/scrape/translations/zh-Hant.json index 499ca44d334..9c101ee5df2 100644 --- a/homeassistant/components/scrape/translations/zh-Hant.json +++ b/homeassistant/components/scrape/translations/zh-Hant.json @@ -3,68 +3,121 @@ "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, + "error": { + "resource_error": "\u7121\u6cd5\u66f4\u65b0\u91cd\u7f6e\u8cc7\u6599\uff0c\u8acb\u78ba\u8a8d\u8a2d\u5b9a\u3002" + }, "step": { - "user": { + "sensor": { "data": { "attribute": "\u5c6c\u6027", - "authentication": "\u9a57\u8b49", "device_class": "\u88dd\u7f6e\u985e\u5225", - "headers": "Headers", "index": "\u6307\u6578", "name": "\u540d\u7a31", - "password": "\u5bc6\u78bc", - "resource": "\u4f86\u6e90", "select": "\u9078\u64c7", "state_class": "\u72c0\u614b\u985e\u5225", "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", - "username": "\u4f7f\u7528\u8005\u540d\u7a31", - "value_template": "\u6578\u503c\u6a21\u677f", - "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + "value_template": "\u6578\u503c\u6a21\u677f" }, "data_description": { "attribute": "\u7372\u53d6\u6240\u9078\u6a19\u7c64\u5c6c\u6027\u6578\u503c", - "authentication": "HTTP \u9a57\u8b49\u985e\u578b\u3002\u57fa\u672c\u6216\u6458\u8981", "device_class": "\u65bc Frontend \u4e2d\u8a2d\u5b9a\u4e4b\u50b3\u611f\u5668\u985e\u578b/\u985e\u5225\u5716\u793a", - "headers": "\u7528\u65bc Web \u8acb\u6c42\u4e4b Headers", "index": "\u5b9a\u7fa9\u4f7f\u7528 CSS selector \u56de\u8986\u5143\u7d20", - "resource": "\u5305\u542b\u6578\u503c\u7684\u7db2\u7ad9 URL", "select": "\u5b9a\u7fa9\u8981\u7d22\u7684\u6a19\u7c64\u3002\u53c3\u95b1 Beautifulsoup CSS selector \u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a", "state_class": "\u611f\u6e2c\u5668 state_class", - "value_template": "\u5b9a\u7fa9\u6a21\u677f\u4ee5\u53d6\u5f97\u611f\u6e2c\u5668\u72c0\u614b", + "unit_of_measurement": "\u9078\u64c7\u6eab\u5ea6\u6e2c\u91cf\u6216\u8005\u5275\u5efa\u81ea\u5b9a\u7fa9", + "value_template": "\u5b9a\u7fa9\u6a21\u677f\u4ee5\u53d6\u5f97\u611f\u6e2c\u5668\u72c0\u614b" + } + }, + "user": { + "data": { + "authentication": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f", + "headers": "Headers", + "method": "\u65b9\u5f0f", + "password": "\u5bc6\u78bc", + "resource": "\u4f86\u6e90", + "timeout": "\u903e\u6642", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "data_description": { + "authentication": "HTTP \u9a57\u8b49\u985e\u578b\u3002\u57fa\u672c\u6216\u6458\u8981", + "headers": "\u7528\u65bc Web \u8acb\u6c42\u4e4b Headers", + "resource": "\u5305\u542b\u6578\u503c\u7684\u7db2\u7ad9 URL", + "timeout": "\u7db2\u7ad9\u9023\u7dda\u903e\u6642", "verify_ssl": "\u958b\u555f/\u95dc\u9589 SSL/TLS \u9a57\u8b49\u8a8d\u8b49\uff0c\u4f8b\u5982\u81ea\u7c3d\u7ae0\u6191\u8b49" } } } }, + "issues": { + "moved_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Scrape \u5373\u5c07\u8f49\u79fb\u81f3\u6574\u5408\u3002\n\n\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u53ea\u80fd\u518d\u4f7f\u7528\u5169\u500b\u66f4\u65b0\u7248\u672c\u3002\n\n\u8ddf\u96a8\u6587\u4ef6\u8aaa\u660e\u9077\u79fb YAML \u8a2d\u5b9a\u81f3\u6574\u5408\u3002", + "title": "Scrape YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { - "init": { + "add_sensor": { "data": { "attribute": "\u5c6c\u6027", - "authentication": "\u9a57\u8b49", "device_class": "\u88dd\u7f6e\u985e\u5225", - "headers": "Headers", "index": "\u6307\u6578", "name": "\u540d\u7a31", - "password": "\u5bc6\u78bc", - "resource": "\u4f86\u6e90", "select": "\u9078\u64c7", "state_class": "\u72c0\u614b\u985e\u5225", "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", - "username": "\u4f7f\u7528\u8005\u540d\u7a31", - "value_template": "\u6578\u503c\u6a21\u677f", - "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + "value_template": "\u6578\u503c\u6a21\u677f" }, "data_description": { "attribute": "\u7372\u53d6\u6240\u9078\u6a19\u7c64\u5c6c\u6027\u6578\u503c", - "authentication": "HTTP \u9a57\u8b49\u985e\u578b\u3002\u57fa\u672c\u6216\u6458\u8981", "device_class": "\u65bc Frontend \u4e2d\u8a2d\u5b9a\u4e4b\u50b3\u611f\u5668\u985e\u578b/\u985e\u5225\u5716\u793a", - "headers": "\u7528\u65bc Web \u8acb\u6c42\u4e4b Headers", "index": "\u5b9a\u7fa9\u4f7f\u7528 CSS selector \u56de\u8986\u5143\u7d20", - "resource": "\u5305\u542b\u6578\u503c\u7684\u7db2\u7ad9 URL", "select": "\u5b9a\u7fa9\u8981\u7d22\u7684\u6a19\u7c64\u3002\u53c3\u95b1 Beautifulsoup CSS selector \u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a", "state_class": "\u611f\u6e2c\u5668 state_class", - "value_template": "\u5b9a\u7fa9\u6a21\u677f\u4ee5\u53d6\u5f97\u611f\u6e2c\u5668\u72c0\u614b", + "unit_of_measurement": "\u9078\u64c7\u6eab\u5ea6\u6e2c\u91cf\u6216\u8005\u5275\u5efa\u81ea\u5b9a\u7fa9", + "value_template": "\u5b9a\u7fa9\u6a21\u677f\u4ee5\u53d6\u5f97\u611f\u6e2c\u5668\u72c0\u614b" + } + }, + "init": { + "data": { + "authentication": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f", + "headers": "Headers", + "method": "\u65b9\u5f0f", + "password": "\u5bc6\u78bc", + "resource": "\u4f86\u6e90", + "timeout": "\u903e\u6642", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "data_description": { + "authentication": "HTTP \u9a57\u8b49\u985e\u578b\u3002\u57fa\u672c\u6216\u6458\u8981", + "headers": "\u7528\u65bc Web \u8acb\u6c42\u4e4b Headers", + "resource": "\u5305\u542b\u6578\u503c\u7684\u7db2\u7ad9 URL", + "timeout": "\u7db2\u7ad9\u9023\u7dda\u903e\u6642", + "verify_ssl": "\u958b\u555f/\u95dc\u9589 SSL/TLS \u9a57\u8b49\u8a8d\u8b49\uff0c\u4f8b\u5982\u81ea\u7c3d\u7ae0\u6191\u8b49" + }, + "menu_options": { + "add_sensor": "\u65b0\u589e\u611f\u6e2c\u5668", + "remove_sensor": "\u79fb\u9664\u611f\u6e2c\u5668", + "resource": "\u8a2d\u5b9a\u4f86\u6e90" + } + }, + "resource": { + "data": { + "authentication": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f", + "headers": "Headers", + "method": "\u65b9\u5f0f", + "password": "\u5bc6\u78bc", + "resource": "\u4f86\u6e90", + "timeout": "\u903e\u6642", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "data_description": { + "authentication": "HTTP \u9a57\u8b49\u985e\u578b\u3002\u57fa\u672c\u6216\u6458\u8981", + "headers": "\u7528\u65bc Web \u8acb\u6c42\u4e4b Headers", + "resource": "\u5305\u542b\u6578\u503c\u7684\u7db2\u7ad9 URL", + "timeout": "\u7db2\u7ad9\u9023\u7dda\u903e\u6642", "verify_ssl": "\u958b\u555f/\u95dc\u9589 SSL/TLS \u9a57\u8b49\u8a8d\u8b49\uff0c\u4f8b\u5982\u81ea\u7c3d\u7ae0\u6191\u8b49" } } diff --git a/homeassistant/components/screenlogic/translations/sk.json b/homeassistant/components/screenlogic/translations/sk.json index f547d5e3a90..88a6f2a1c76 100644 --- a/homeassistant/components/screenlogic/translations/sk.json +++ b/homeassistant/components/screenlogic/translations/sk.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", "step": { "gateway_entry": { "data": { + "ip_address": "IP adresa", "port": "Port" } + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + } } } } diff --git a/homeassistant/components/season/translations/sensor.sk.json b/homeassistant/components/season/translations/sensor.sk.json index 6a1f5dd293b..58cc37ac3dd 100644 --- a/homeassistant/components/season/translations/sensor.sk.json +++ b/homeassistant/components/season/translations/sensor.sk.json @@ -1,7 +1,16 @@ { "state": { + "season__season": { + "autumn": "Jese\u0148", + "spring": "Jar", + "summer": "Leto", + "winter": "Zima" + }, "season__season__": { - "spring": "Jar" + "autumn": "Jese\u0148", + "spring": "Jar", + "summer": "Leto", + "winter": "Zima" } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sk.json b/homeassistant/components/season/translations/sk.json new file mode 100644 index 00000000000..2e87a326f7c --- /dev/null +++ b/homeassistant/components/season/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "user": { + "data": { + "type": "Typ defin\u00edcie ro\u010dn\u00e9ho obdobia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/sk.json b/homeassistant/components/select/translations/sk.json new file mode 100644 index 00000000000..220711deee0 --- /dev/null +++ b/homeassistant/components/select/translations/sk.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "selected_option": "Aktu\u00e1lna vybrat\u00e1 mo\u017enos\u0165 {entity_name}" + }, + "trigger_type": { + "current_option_changed": "Mo\u017enos\u0165 {entity_name} sa zmenila" + } + }, + "title": "Vyberte" +} \ No newline at end of file diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 82b25802b02..316cbb841ed 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -66,6 +66,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: access_token = entry_data.get("access_token", "") user_id = entry_data.get("user_id", "") + device_id = entry_data.get("device_id", "") + refresh_token = entry_data.get("refresh_token", "") monitor_id = entry_data.get("monitor_id", "") client_session = async_get_clientsession(hass) @@ -76,7 +78,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway.rate_limit = ACTIVE_UPDATE_RATE try: - gateway.load_auth(access_token, user_id, monitor_id) + gateway.load_auth(access_token, user_id, device_id, refresh_token) + gateway.set_monitor_id(monitor_id) await gateway.get_monitor_data() except (SenseAuthenticationException, SenseMFARequiredException) as err: _LOGGER.warning("Sense authentication expired") diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 0690344ccf1..d7f7588beb2 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -60,6 +60,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Create the entry from the config data.""" self._auth_data["access_token"] = self._gateway.sense_access_token self._auth_data["user_id"] = self._gateway.sense_user_id + self._auth_data["device_id"] = self._gateway.device_id + self._auth_data["refresh_token"] = self._gateway.refresh_token self._auth_data["monitor_id"] = self._gateway.sense_monitor_id existing_entry = await self.async_set_unique_id(self._auth_data[CONF_EMAIL]) if not existing_entry: diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 192a0f6defe..158ef7cae61 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.10.4"], + "requirements": ["sense_energy==0.11.0"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json index 91bf013c047..f93c252a662 100644 --- a/homeassistant/components/sense/translations/bg.json +++ b/homeassistant/components/sense/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/sense/translations/sk.json b/homeassistant/components/sense/translations/sk.json index 72b0304f1c3..1a1a1e64869 100644 --- a/homeassistant/components/sense/translations/sk.json +++ b/homeassistant/components/sense/translations/sk.json @@ -1,12 +1,31 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { + "reauth_validate": { + "data": { + "password": "Heslo" + }, + "description": "Integr\u00e1cia Sense vy\u017eaduje op\u00e4tovn\u00e9 overenie v\u00e1\u0161ho \u00fa\u010dtu {email}.", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" + } + }, + "validation": { + "data": { + "code": "Overovac\u00ed k\u00f3d" } } } diff --git a/homeassistant/components/senseme/translations/de.json b/homeassistant/components/senseme/translations/de.json index 01463118ec0..316b7b3b437 100644 --- a/homeassistant/components/senseme/translations/de.json +++ b/homeassistant/components/senseme/translations/de.json @@ -11,7 +11,7 @@ "flow_title": "{name} - {model} ({host})", "step": { "discovery_confirm": { - "description": "M\u00f6chtest du {name} - {model} ( {host} ) einrichten?" + "description": "M\u00f6chtest du {name} - {model} ({host}) einrichten?" }, "manual": { "data": { @@ -23,7 +23,7 @@ "data": { "device": "Ger\u00e4t" }, - "description": "W\u00e4hle ein Ger\u00e4t aus, oder w\u00e4hle \"IP-Adresse\", um eine IP-Adresse manuell einzugeben." + "description": "W\u00e4hle ein Ger\u00e4t aus oder w\u00e4hle \"IP-Adresse\", um eine IP-Adresse manuell einzugeben." } } } diff --git a/homeassistant/components/senseme/translations/ru.json b/homeassistant/components/senseme/translations/ru.json index 8debd33481f..905f670a1f9 100644 --- a/homeassistant/components/senseme/translations/ru.json +++ b/homeassistant/components/senseme/translations/ru.json @@ -6,7 +6,7 @@ }, "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_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{name} - {model} ({host})", "step": { diff --git a/homeassistant/components/senseme/translations/sk.json b/homeassistant/components/senseme/translations/sk.json index ffc3e13321e..03e144be233 100644 --- a/homeassistant/components/senseme/translations/sk.json +++ b/homeassistant/components/senseme/translations/sk.json @@ -1,7 +1,30 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "error": { - "cannot_connect": "Nepodarilo sa pripoji\u0165." + "cannot_connect": "Nepodarilo sa pripoji\u0165.", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa" + }, + "flow_title": "{name} - {model} ({host})", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} - {model} ({host})?" + }, + "manual": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Zadajte IP adresu." + }, + "user": { + "data": { + "device": "Zariadenie" + }, + "description": "Vyberte zariadenie alebo vyberte mo\u017enos\u0165 \"IP adresa\", ak chcete manu\u00e1lne zada\u0165 IP adresu." + } } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 70d4a12406a..b1980b5e595 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -176,9 +176,9 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): self._attr_supported_features = self.get_features() self._attr_precision = PRECISION_TENTHS - def get_features(self) -> int: + def get_features(self) -> ClimateEntityFeature: """Get supported features.""" - features = 0 + features = ClimateEntityFeature(0) for key in self.device_data.full_features: if key in FIELD_TO_FLAG: features |= FIELD_TO_FLAG[key] diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 22981ada51f..ad5286a02c2 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -204,7 +204,56 @@ AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( ), ) -DESCRIPTION_BY_MODELS = {"pure": PURE_SENSOR_TYPES, "airq": AIRQ_SENSOR_TYPES} +ELEMENT_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( + SensiboDeviceSensorEntityDescription( + key="pm25", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + name="PM 2.5", + value_fn=lambda data: data.pm25, + extra_fn=None, + ), + SensiboDeviceSensorEntityDescription( + key="tvoc", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + name="TVOC", + value_fn=lambda data: data.tvoc, + extra_fn=None, + ), + SensiboDeviceSensorEntityDescription( + key="co2", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + name="CO2", + value_fn=lambda data: data.co2, + extra_fn=None, + ), + SensiboDeviceSensorEntityDescription( + key="ethanol", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + name="Ethanol", + value_fn=lambda data: data.etoh, + extra_fn=None, + ), + SensiboDeviceSensorEntityDescription( + key="iaq", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + name="Air quality", + value_fn=lambda data: data.iaq, + extra_fn=None, + ), +) + +DESCRIPTION_BY_MODELS = { + "pure": PURE_SENSOR_TYPES, + "airq": AIRQ_SENSOR_TYPES, + "elements": ELEMENT_SENSOR_TYPES, +} async def async_setup_entry( diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index 6bdc0050748..3e64be8cd23 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index fc33b6e7698..64558ebb852 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -17,7 +17,7 @@ "api_key": "Kunci API" }, "data_description": { - "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API baru." + "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API" } }, "user": { @@ -25,7 +25,7 @@ "api_key": "Kunci API" }, "data_description": { - "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API." + "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API" } } } diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index f2c45f9b5ae..8b34b5eaf50 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -17,7 +17,7 @@ "api_key": "Chiave API" }, "data_description": { - "api_key": "Segui la documentazione per ottenere una nuova chiave API." + "api_key": "Segui la documentazione per ottenere la tua chiave API." } }, "user": { diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index a374be60719..b478b6095f8 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -17,7 +17,7 @@ "api_key": "\u041a\u043b\u044e\u0447 API" }, "data_description": { - "api_key": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API." + "api_key": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" } }, "user": { @@ -25,7 +25,7 @@ "api_key": "\u041a\u043b\u044e\u0447 API" }, "data_description": { - "api_key": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." + "api_key": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" } } } diff --git a/homeassistant/components/sensibo/translations/sensor.cs.json b/homeassistant/components/sensibo/translations/sensor.cs.json new file mode 100644 index 00000000000..12454dcbf68 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.cs.json @@ -0,0 +1,9 @@ +{ + "state": { + "sensibo__smart_type": { + "feelslike": "Pocitov\u011b", + "humidity": "Vlhkost vzduchu", + "temperature": "Teplota" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.el.json b/homeassistant/components/sensibo/translations/sensor.el.json index b4e595db882..87adea95b4a 100644 --- a/homeassistant/components/sensibo/translations/sensor.el.json +++ b/homeassistant/components/sensibo/translations/sensor.el.json @@ -3,6 +3,11 @@ "sensibo__sensitivity": { "n": "\u039a\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03cc", "s": "\u0395\u03c5\u03b1\u03af\u03c3\u03b8\u03b7\u03c4\u03bf" + }, + "sensibo__smart_type": { + "feelslike": "\u0391\u03af\u03c3\u03b8\u03b7\u03c3\u03b7 \u03c3\u03b1\u03bd", + "humidity": "\u03a5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1", + "temperature": "\u0398\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1" } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.he.json b/homeassistant/components/sensibo/translations/sensor.he.json new file mode 100644 index 00000000000..3a45f273e0d --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.he.json @@ -0,0 +1,7 @@ +{ + "state": { + "sensibo__smart_type": { + "temperature": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.hr.json b/homeassistant/components/sensibo/translations/sensor.hr.json new file mode 100644 index 00000000000..26f61fd8d34 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.hr.json @@ -0,0 +1,9 @@ +{ + "state": { + "sensibo__smart_type": { + "feelslike": "Osje\u0107aj", + "humidity": "Vla\u017enost", + "temperature": "Temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.id.json b/homeassistant/components/sensibo/translations/sensor.id.json index 54a0554ce41..29fb9777117 100644 --- a/homeassistant/components/sensibo/translations/sensor.id.json +++ b/homeassistant/components/sensibo/translations/sensor.id.json @@ -3,6 +3,11 @@ "sensibo__sensitivity": { "n": "Normal", "s": "Sensitif" + }, + "sensibo__smart_type": { + "feelslike": "Terasa seperti", + "humidity": "Kelembaban", + "temperature": "Suhu" } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.it.json b/homeassistant/components/sensibo/translations/sensor.it.json index cb611c4cf42..1da3b54d850 100644 --- a/homeassistant/components/sensibo/translations/sensor.it.json +++ b/homeassistant/components/sensibo/translations/sensor.it.json @@ -5,7 +5,7 @@ "s": "Sensibile" }, "sensibo__smart_type": { - "feelslike": "Sembra che", + "feelslike": "Percepita", "humidity": "Umidit\u00e0", "temperature": "Temperatura" } diff --git a/homeassistant/components/sensibo/translations/sensor.nl.json b/homeassistant/components/sensibo/translations/sensor.nl.json index bd7b06dc940..e2ad9aebd5d 100644 --- a/homeassistant/components/sensibo/translations/sensor.nl.json +++ b/homeassistant/components/sensibo/translations/sensor.nl.json @@ -3,6 +3,10 @@ "sensibo__sensitivity": { "n": "Normaal", "s": "Gevoelig" + }, + "sensibo__smart_type": { + "humidity": "Vochtigheid", + "temperature": "Temperatuur" } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.sk.json b/homeassistant/components/sensibo/translations/sensor.sk.json new file mode 100644 index 00000000000..badb719530c --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.sk.json @@ -0,0 +1,13 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Norm\u00e1lne", + "s": "Citliv\u00e9" + }, + "sensibo__smart_type": { + "feelslike": "Pocitovo", + "humidity": "Vlhkos\u0165", + "temperature": "Teplota" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.tr.json b/homeassistant/components/sensibo/translations/sensor.tr.json index 3364a75abe2..bb9f87e0063 100644 --- a/homeassistant/components/sensibo/translations/sensor.tr.json +++ b/homeassistant/components/sensibo/translations/sensor.tr.json @@ -3,6 +3,11 @@ "sensibo__sensitivity": { "n": "Normal", "s": "Duyarl\u0131" + }, + "sensibo__smart_type": { + "feelslike": "Hissedilen", + "humidity": "Nem", + "temperature": "S\u0131cakl\u0131k" } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sk.json b/homeassistant/components/sensibo/translations/sk.json index d2401d8b7a3..cc38d3ef6f5 100644 --- a/homeassistant/components/sensibo/translations/sk.json +++ b/homeassistant/components/sensibo/translations/sk.json @@ -1,12 +1,31 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, "error": { - "incorrect_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d pre vybran\u00fd \u00fa\u010det" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "incorrect_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d pre vybran\u00fd \u00fa\u010det", + "invalid_auth": "Neplatn\u00e9 overenie", + "no_devices": "Nena\u0161li sa \u017eiadne zariadenia", + "no_username": "Nepodarilo sa z\u00edska\u0165 pou\u017e\u00edvate\u013esk\u00e9 meno" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + }, + "data_description": { + "api_key": "Ak chcete z\u00edska\u0165 k\u013e\u00fa\u010d API, postupujte pod\u013ea dokument\u00e1cie" + } + }, "user": { "data": { "api_key": "API k\u013e\u00fa\u010d" + }, + "data_description": { + "api_key": "Ak chcete z\u00edska\u0165 k\u013e\u00fa\u010d API, postupujte pod\u013ea dokument\u00e1cie" } } } diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json index 47270a1dece..fc2abb330e4 100644 --- a/homeassistant/components/sensibo/translations/tr.json +++ b/homeassistant/components/sensibo/translations/tr.json @@ -17,7 +17,7 @@ "api_key": "API Anahtar\u0131" }, "data_description": { - "api_key": "Yeni bir api anahtar\u0131 almak i\u00e7in belgeleri izleyin." + "api_key": "API anahtar\u0131n\u0131z\u0131 almak i\u00e7in belgeleri izleyin" } }, "user": { @@ -25,7 +25,7 @@ "api_key": "API Anahtar\u0131" }, "data_description": { - "api_key": "API anahtar\u0131n\u0131z\u0131 almak i\u00e7in belgeleri izleyin." + "api_key": "API anahtar\u0131n\u0131z\u0131 almak i\u00e7in belgeleri izleyin" } } } diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 4ad41a2e3f3..b40fad85fad 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -17,7 +17,7 @@ "api_key": "API \u91d1\u9470" }, "data_description": { - "api_key": "\u8acb\u8ddf\u96a8\u6587\u4ef6\u4ee5\u53d6\u5f97\u65b0 API \u91d1\u9470\u3002" + "api_key": "\u8acb\u8ddf\u96a8\u6587\u4ef6\u4ee5\u53d6\u5f97 API \u91d1\u9470" } }, "user": { @@ -25,7 +25,7 @@ "api_key": "API \u91d1\u9470" }, "data_description": { - "api_key": "\u8acb\u8ddf\u96a8\u6587\u4ef6\u4ee5\u53d6\u5f97 API \u91d1\u9470\u3002" + "api_key": "\u8acb\u8ddf\u96a8\u6587\u4ef6\u4ee5\u53d6\u5f97 API \u91d1\u9470" } } } diff --git a/homeassistant/components/sensirion_ble/__init__.py b/homeassistant/components/sensirion_ble/__init__.py new file mode 100644 index 00000000000..66e6f7c250b --- /dev/null +++ b/homeassistant/components/sensirion_ble/__init__.py @@ -0,0 +1,49 @@ +"""The sensirion_ble integration.""" +from __future__ import annotations + +import logging + +from sensirion_ble import SensirionBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Sensirion BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = SensirionBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/sensirion_ble/config_flow.py b/homeassistant/components/sensirion_ble/config_flow.py new file mode 100644 index 00000000000..0442b6d16c3 --- /dev/null +++ b/homeassistant/components/sensirion_ble/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for sensirion_ble.""" +from __future__ import annotations + +from typing import Any + +from sensirion_ble import SensirionBluetoothDeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class SensirionConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for sensirion_ble.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: SensirionBluetoothDeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = SensirionBluetoothDeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = SensirionBluetoothDeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/sensirion_ble/const.py b/homeassistant/components/sensirion_ble/const.py new file mode 100644 index 00000000000..58403948762 --- /dev/null +++ b/homeassistant/components/sensirion_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the sensirion_ble integration.""" + +DOMAIN = "sensirion_ble" diff --git a/homeassistant/components/sensirion_ble/manifest.json b/homeassistant/components/sensirion_ble/manifest.json new file mode 100644 index 00000000000..f13f393a844 --- /dev/null +++ b/homeassistant/components/sensirion_ble/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "sensirion_ble", + "name": "Sensirion BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sensirion_ble", + "bluetooth": [ + { + "manufacturer_id": 1749 + }, + { + "local_name": "MyCO2*" + } + ], + "requirements": ["sensirion-ble==0.0.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@akx"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/sensirion_ble/sensor.py b/homeassistant/components/sensirion_ble/sensor.py new file mode 100644 index 00000000000..3612b25f341 --- /dev/null +++ b/homeassistant/components/sensirion_ble/sensor.py @@ -0,0 +1,131 @@ +"""Support for Sensirion sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from sensor_state_data import ( + DeviceKey, + SensorDescription, + SensorDeviceClass as SSDSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries, const +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS: dict[ + tuple[SSDSensorDeviceClass, Units | None], SensorEntityDescription +] = { + ( + SSDSensorDeviceClass.CO2, + Units.CONCENTRATION_PARTS_PER_MILLION, + ): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=const.CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + (SSDSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=const.PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (SSDSensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{SSDSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=const.TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _to_sensor_key( + description: SensorDescription, +) -> tuple[SSDSensorDeviceClass, Units | None]: + assert description.device_class is not None + return (description.device_class, description.native_unit_of_measurement) + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass_device_info(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + _to_sensor_key(description) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if _to_sensor_key(description) in SENSOR_DESCRIPTIONS + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Sensirion BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + SensirionBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class SensirionBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Sensirion BLE sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 6ba88defc83..a1a7e9ce1a7 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -88,6 +88,30 @@ SCAN_INTERVAL: Final = timedelta(seconds=30) class SensorDeviceClass(StrEnum): """Device class for sensors.""" + # Non-numerical device classes + DATE = "date" + """Date. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ + + DURATION = "duration" + """Fixed duration. + + Unit of measurement: `d`, `h`, `min`, `s` + """ + + TIMESTAMP = "timestamp" + """Timestamp. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ + + # Numerical device classes, these should be aligned with NumberDeviceClass APPARENT_POWER = "apparent_power" """Apparent power. @@ -124,14 +148,6 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `A` """ - DATE = "date" - """Date. - - Unit of measurement: `None` - - ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 - """ - DISTANCE = "distance" """Generic distance. @@ -140,12 +156,6 @@ class SensorDeviceClass(StrEnum): - USCS / imperial: `in`, `ft`, `yd`, `mi` """ - DURATION = "duration" - """Fixed duration. - - Unit of measurement: `d`, `h`, `min`, `s` - """ - ENERGY = "energy" """Energy. @@ -244,6 +254,14 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `W`, `kW` """ + PRECIPITATION = "precipitation" + """Precipitation. + + Unit of measurement: + - SI / metric: `mm` + - USCS / imperial: `in` + """ + PRECIPITATION_INTENSITY = "precipitation_intensity" """Precipitation intensity. @@ -295,14 +313,6 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `°C`, `°F` """ - TIMESTAMP = "timestamp" - """Timestamp. - - Unit of measurement: `None` - - ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 - """ - VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. @@ -392,6 +402,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.GAS: VolumeConverter, + SensorDeviceClass.PRECIPITATION: DistanceConverter, SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index e06355dda8c..34a2590ce8e 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -52,6 +52,7 @@ CONF_IS_PM10 = "is_pm10" CONF_IS_PM25 = "is_pm25" CONF_IS_POWER = "is_power" CONF_IS_POWER_FACTOR = "is_power_factor" +CONF_IS_PRECIPITATION = "is_precipitation" CONF_IS_PRECIPITATION_INTENSITY = "is_precipitation_intensity" CONF_IS_PRESSURE = "is_pressure" CONF_IS_SPEED = "is_speed" @@ -89,6 +90,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.PM1: [{CONF_TYPE: CONF_IS_PM1}], SensorDeviceClass.PM10: [{CONF_TYPE: CONF_IS_PM10}], SensorDeviceClass.PM25: [{CONF_TYPE: CONF_IS_PM25}], + SensorDeviceClass.PRECIPITATION: [{CONF_TYPE: CONF_IS_PRECIPITATION}], SensorDeviceClass.PRECIPITATION_INTENSITY: [ {CONF_TYPE: CONF_IS_PRECIPITATION_INTENSITY} ], @@ -126,16 +128,18 @@ CONDITION_SCHEMA = vol.All( CONF_IS_GAS, CONF_IS_HUMIDITY, CONF_IS_ILLUMINANCE, - CONF_IS_OZONE, CONF_IS_MOISTURE, CONF_IS_NITROGEN_DIOXIDE, CONF_IS_NITROGEN_MONOXIDE, CONF_IS_NITROUS_OXIDE, + CONF_IS_OZONE, CONF_IS_POWER, CONF_IS_POWER_FACTOR, CONF_IS_PM1, CONF_IS_PM10, CONF_IS_PM25, + CONF_IS_PRECIPITATION, + CONF_IS_PRECIPITATION_INTENSITY, CONF_IS_PRESSURE, CONF_IS_REACTIVE_POWER, CONF_IS_SIGNAL_STRENGTH, @@ -143,6 +147,10 @@ CONDITION_SCHEMA = vol.All( CONF_IS_TEMPERATURE, CONF_IS_VOLATILE_ORGANIC_COMPOUNDS, CONF_IS_VOLTAGE, + CONF_IS_VOLUME, + CONF_IS_WATER, + CONF_IS_WEIGHT, + CONF_IS_WIND_SPEED, CONF_IS_VALUE, ] ), diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 4f36c8b78cb..c1b0699664e 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -51,6 +51,7 @@ CONF_PM10 = "pm10" CONF_PM25 = "pm25" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" +CONF_PRECIPITATION = "precipitation" CONF_PRECIPITATION_INTENSITY = "precipitation_intensity" CONF_PRESSURE = "pressure" CONF_REACTIVE_POWER = "reactive_power" @@ -88,6 +89,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.PM25: [{CONF_TYPE: CONF_PM25}], SensorDeviceClass.POWER: [{CONF_TYPE: CONF_POWER}], SensorDeviceClass.POWER_FACTOR: [{CONF_TYPE: CONF_POWER_FACTOR}], + SensorDeviceClass.PRECIPITATION: [{CONF_TYPE: CONF_PRECIPITATION}], SensorDeviceClass.PRECIPITATION_INTENSITY: [ {CONF_TYPE: CONF_PRECIPITATION_INTENSITY} ], @@ -136,6 +138,8 @@ TRIGGER_SCHEMA = vol.All( CONF_PM25, CONF_POWER, CONF_POWER_FACTOR, + CONF_PRECIPITATION, + CONF_PRECIPITATION_INTENSITY, CONF_PRESSURE, CONF_REACTIVE_POWER, CONF_SIGNAL_STRENGTH, @@ -143,6 +147,10 @@ TRIGGER_SCHEMA = vol.All( CONF_TEMPERATURE, CONF_VOLATILE_ORGANIC_COMPOUNDS, CONF_VOLTAGE, + CONF_VOLUME, + CONF_WATER, + CONF_WEIGHT, + CONF_WIND_SPEED, CONF_VALUE, ] ), diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 5e853ec3c74..f4b3897492d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -450,7 +450,7 @@ def _compile_statistics( # noqa: C901 to_query.append(entity_id) last_stats = statistics.get_latest_short_term_statistics( - hass, to_query, metadata=old_metadatas + hass, to_query, {"last_reset", "state", "sum"}, metadata=old_metadatas ) for ( # pylint: disable=too-many-nested-blocks entity_id, @@ -508,6 +508,8 @@ def _compile_statistics( # noqa: C901 if entity_id in last_stats: # We have compiled history for this sensor before, use that as a starting point last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] + if old_last_reset is not None: + last_reset = old_last_reset = old_last_reset.isoformat() new_state = old_state = last_stats[entity_id][0]["state"] _sum = last_stats[entity_id][0]["sum"] or 0.0 diff --git a/homeassistant/components/sensor/translations/bg.json b/homeassistant/components/sensor/translations/bg.json index f4ea74ca57e..c72139b3552 100644 --- a/homeassistant/components/sensor/translations/bg.json +++ b/homeassistant/components/sensor/translations/bg.json @@ -12,7 +12,7 @@ "is_value": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}" }, "trigger_type": { - "battery_level": "{entity_name} \u043d\u0438\u0432\u043e\u0442\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u044f", + "battery_level": "{entity_name} \u043f\u0440\u0438 \u043f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u043d\u0438\u0432\u043e\u0442\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430", "humidity": "{entity_name} \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", "illuminance": "{entity_name} \u043e\u0441\u0432\u0435\u0442\u0435\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", "power": "\u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index d8f6dd25b57..240f03942ce 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -18,7 +18,8 @@ "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}", "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}", - "is_volume": "Aktu\u00e1ln\u00ed objem {entity_name}" + "is_volume": "Aktu\u00e1ln\u00ed objem {entity_name}", + "is_water": "Aktu\u00e1ln\u00ed mno\u017estv\u00ed vody {entity_name}" }, "trigger_type": { "battery_level": "P\u0159i zm\u011bn\u011b \u00farovn\u011b baterie {entity_name}", @@ -38,7 +39,8 @@ "temperature": "P\u0159i zm\u011bn\u011b teploty {entity_name}", "value": "P\u0159i zm\u011bn\u011b hodnoty {entity_name}", "voltage": "P\u0159i zm\u011bn\u011b nap\u011bt\u00ed {entity_name}", - "volume": "P\u0159i zm\u011bn\u011b objemu {entity_name}" + "volume": "P\u0159i zm\u011bn\u011b objemu {entity_name}", + "water": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed vody {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index 6acf181aad3..6e31e95bf6b 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -32,6 +32,7 @@ "is_volatile_organic_compounds": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c0\u03c4\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03bf\u03c1\u03b3\u03b1\u03bd\u03b9\u03ba\u03ce\u03bd \u03b5\u03bd\u03ce\u03c3\u03b5\u03c9\u03bd {entity_name}", "is_voltage": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c4\u03ac\u03c3\u03b7 {entity_name}", "is_volume": "\u03a4\u03c1\u03ad\u03c7\u03c9\u03bd \u03cc\u03b3\u03ba\u03bf\u03c2 {entity_name}", + "is_water": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03bd\u03b5\u03c1\u03cc {entity_name}", "is_weight": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b2\u03ac\u03c1\u03bf\u03c2 {entity_name}" }, "trigger_type": { @@ -66,6 +67,7 @@ "volatile_organic_compounds": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c0\u03c4\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03bf\u03c1\u03b3\u03b1\u03bd\u03b9\u03ba\u03ce\u03bd \u03b5\u03bd\u03ce\u03c3\u03b5\u03c9\u03bd {entity_name}", "voltage": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03ac\u03c3\u03b7\u03c2 {entity_name}", "volume": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03cc\u03b3\u03ba\u03bf\u03c5 {entity_name}", + "water": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03bd\u03b5\u03c1\u03bf\u03cd {entity_name}", "weight": "\u03a4\u03bf \u03b2\u03ac\u03c1\u03bf\u03c2 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9" } }, diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index 6adc9a6da87..a3dda234d0c 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -28,6 +28,7 @@ "is_value": "\u05e2\u05e8\u05da \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", "is_volatile_organic_compounds": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05ea\u05e8\u05db\u05d5\u05d1\u05d5\u05ea \u05d4\u05d0\u05d5\u05e8\u05d2\u05e0\u05d9\u05d5\u05ea \u05d4\u05e0\u05d3\u05d9\u05e4\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05d5\u05ea {entity_name}", "is_volume": "\u05e0\u05e4\u05d7 \u05e0\u05d5\u05db\u05d7\u05d9 \u05e9\u05dc {entity_name}", + "is_water": "\u05de\u05d9\u05dd \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", "is_weight": "\u05de\u05e9\u05e7\u05dc \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}" }, "trigger_type": { @@ -40,6 +41,7 @@ "gas": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d2\u05d6", "humidity": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05d5\u05ea", "illuminance": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05e8\u05d4", + "moisture": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05d5\u05ea", "nitrogen_dioxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d7\u05e0\u05e7\u05df \u05d4\u05d3\u05d5-\u05d7\u05de\u05e6\u05e0\u05d9", "nitrogen_monoxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d7\u05d3 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05d7\u05e0\u05e7\u05df", "nitrous_oxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05d7\u05e0\u05e7\u05df", @@ -58,6 +60,7 @@ "value": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05e8\u05da", "voltage": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05ea\u05d7", "volume": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e0\u05e4\u05d7", + "water": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05d9\u05dd", "weight": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05de\u05e9\u05e7\u05dc" } }, diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index e9e2340fed0..7a782767547 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -32,6 +32,7 @@ "is_volatile_organic_compounds": "Tingkat konsentrasi senyawa organik volatil {entity_name} saat ini", "is_voltage": "Tegangan {entity_name} saat ini", "is_volume": "Volume {entity_name} saat ini", + "is_water": "Air pada {entity_name} saat ini", "is_weight": "Berat {entity_name} saat ini" }, "trigger_type": { @@ -66,6 +67,7 @@ "volatile_organic_compounds": "Perubahan konsentrasi senyawa organik volatil {entity_name}", "voltage": "Perubahan tegangan {entity_name}", "volume": "Perubahan volume {entity_name}", + "water": "Perubahan air {entity_name}", "weight": "Perubahan berat {entity_name}" } }, diff --git a/homeassistant/components/sensor/translations/is.json b/homeassistant/components/sensor/translations/is.json index 0444c7b2866..1739ee8a3c3 100644 --- a/homeassistant/components/sensor/translations/is.json +++ b/homeassistant/components/sensor/translations/is.json @@ -1,4 +1,9 @@ { + "device_automation": { + "condition_type": { + "is_water": "N\u00faverandi {entity_name} vatn" + } + }, "state": { "_": { "off": "Af", diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index ca319a3437a..edd6b85c4ff 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -32,6 +32,7 @@ "is_volatile_organic_compounds": "Attuale livello di concentrazione di composti organici volatili di {entity_name}", "is_voltage": "Tensione attuale di {entity_name}", "is_volume": "Volume attuale di {entity_name}", + "is_water": "Attuale {entity_name} acqua", "is_weight": "Peso attuale di {entity_name}" }, "trigger_type": { @@ -66,6 +67,7 @@ "volatile_organic_compounds": "Variazioni della concentrazione di composti organici volatili di {entity_name}", "voltage": "variazioni di tensione di {entity_name}", "volume": "Variazioni di volume di {entity_name}", + "water": "{entity_name} variazioni d'acqua", "weight": "Variazioni di peso di {entity_name}" } }, diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index 9b7a1f7dbe8..1961ef86ca0 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -32,6 +32,7 @@ "is_volatile_organic_compounds": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043b\u0435\u0442\u0443\u0447\u0438\u0445 \u043e\u0440\u0433\u0430\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0432\u0435\u0449\u0435\u0441\u0442\u0432", "is_voltage": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", "is_volume": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_water": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_weight": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" }, "trigger_type": { @@ -66,6 +67,7 @@ "volatile_organic_compounds": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043b\u0435\u0442\u0443\u0447\u0438\u0445 \u043e\u0440\u0433\u0430\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0432\u0435\u0449\u0435\u0441\u0442\u0432", "voltage": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", "volume": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043e\u0431\u044a\u0451\u043c", + "water": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "weight": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u0435\u0441" } }, diff --git a/homeassistant/components/sensor/translations/sk.json b/homeassistant/components/sensor/translations/sk.json index f86600559a4..d6e664f221d 100644 --- a/homeassistant/components/sensor/translations/sk.json +++ b/homeassistant/components/sensor/translations/sk.json @@ -1,10 +1,51 @@ { "device_automation": { "condition_type": { - "is_voltage": "S\u00fa\u010dasn\u00e9 nap\u00e4tie {entity_name}" + "is_apparent_power": "Aktu\u00e1lny zdanliv\u00fd v\u00fdkon {entity_name}", + "is_battery_level": "Aktu\u00e1lna \u00farove\u0148 nabitia bat\u00e9rie {entity_name}", + "is_carbon_dioxide": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie oxidu uhli\u010dit\u00e9ho {entity_name}", + "is_carbon_monoxide": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie oxidu uho\u013enat\u00e9ho {entity_name}", + "is_current": "Aktu\u00e1lny {entity_name} pr\u00fad", + "is_distance": "Aktu\u00e1lna vzdialenos\u0165 {entity_name}", + "is_energy": "Aktu\u00e1lna energia {entity_name}", + "is_frequency": "Aktu\u00e1lna frekvencia {entity_name}", + "is_gas": "Aktu\u00e1lny plyn {entity_name}", + "is_humidity": "Aktu\u00e1lna vlhkos\u0165 {entity_name}", + "is_illuminance": "Aktu\u00e1lne osvetlenie {entity_name}", + "is_moisture": "Aktu\u00e1lna vlhkos\u0165 {entity_name}", + "is_nitrogen_dioxide": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie oxidu dusi\u010dit\u00e9ho {entity_name}", + "is_nitrogen_monoxide": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie oxidu uho\u013enat\u00e9ho {entity_name}", + "is_nitrous_oxide": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie oxidu dusn\u00e9ho {entity_name}", + "is_ozone": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie oz\u00f3nu {entity_name}", + "is_pm1": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie {entity_name} PM1", + "is_pm10": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie {entity_name} PM10", + "is_pm25": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie {entity_name} PM2,5", + "is_power": "Aktu\u00e1lny v\u00fdkon {entity_name}", + "is_power_factor": "Aktu\u00e1lny \u00fa\u010dinn\u00edk {entity_name}", + "is_pressure": "Aktu\u00e1lny tlak {entity_name}", + "is_reactive_power": "Aktu\u00e1lny jalov\u00fd v\u00fdkon {entity_name}", + "is_signal_strength": "Aktu\u00e1lna sila sign\u00e1lu {entity_name}", + "is_speed": "Aktu\u00e1lna r\u00fdchlos\u0165 {entity_name}", + "is_sulphur_dioxide": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie oxidu siri\u010dit\u00e9ho {entity_name}", + "is_temperature": "Aktu\u00e1lna teplota {entity_name}", + "is_value": "Aktu\u00e1lna hodnota {entity_name}", + "is_volatile_organic_compounds": "Aktu\u00e1lna \u00farove\u0148 koncentr\u00e1cie prchav\u00fdch organick\u00fdch zl\u00fa\u010den\u00edn {entity_name}", + "is_voltage": "S\u00fa\u010dasn\u00e9 nap\u00e4tie {entity_name}", + "is_volume": "Aktu\u00e1lny oddiel {entity_name}", + "is_water": "Aktu\u00e1lna voda {entity_name}", + "is_weight": "Aktu\u00e1lna hmotnos\u0165 {entity_name}" }, "trigger_type": { + "carbon_dioxide": "{entity_name} sa men\u00ed koncentr\u00e1cia oxidu uhli\u010dit\u00e9ho", + "carbon_monoxide": "{entity_name} sa men\u00ed koncentr\u00e1cia oxidu uho\u013enat\u00e9ho", "current": "{entity_name} pr\u00fad sa zmen\u00ed", + "humidity": "{n\u00e1zov_objektu} zmeny vlhkosti", + "nitrogen_dioxide": "{entity_name} zmeny koncentr\u00e1cie oxidu dusi\u010dit\u00e9ho", + "nitrogen_monoxide": "{entity_name} zmeny koncentr\u00e1cie oxidu dusnat\u00e9ho", + "nitrous_oxide": "{entity_name} zmeny koncentr\u00e1cie oxidu dusn\u00e9ho", + "sulphur_dioxide": "{entity_name} zmeny koncentr\u00e1cie oxidu siri\u010dit\u00e9ho", + "temperature": "Zmena teploty {entity_name}", + "value": "Zmena hodnoty {entity_name}", "voltage": "{entity_name} zmen\u00ed nap\u00e4tie" } }, diff --git a/homeassistant/components/sensorpro/device.py b/homeassistant/components/sensorpro/device.py index b5b44eef50f..326eb8b8bbd 100644 --- a/homeassistant/components/sensorpro/device.py +++ b/homeassistant/components/sensorpro/device.py @@ -1,13 +1,11 @@ """Support for SensorPro devices.""" from __future__ import annotations -from sensorpro_ble import DeviceKey, SensorDeviceInfo +from sensorpro_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo def device_key_to_bluetooth_entity_key( @@ -15,17 +13,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensorpro device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/sensorpro/sensor.py b/homeassistant/components/sensorpro/sensor.py index 8866ed44587..deb79e62bdc 100644 --- a/homeassistant/components/sensorpro/sensor.py +++ b/homeassistant/components/sensorpro/sensor.py @@ -31,9 +31,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { (SensorProSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( @@ -87,7 +88,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/sensorpro/translations/he.json b/homeassistant/components/sensorpro/translations/he.json index b182a698234..e34a0c9d525 100644 --- a/homeassistant/components/sensorpro/translations/he.json +++ b/homeassistant/components/sensorpro/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" }, "flow_title": "{name}", diff --git a/homeassistant/components/sensorpro/translations/sk.json b/homeassistant/components/sensorpro/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 8a4db7aff14..58b569d5227 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from sensorpush_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -20,17 +20,14 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN @@ -73,27 +70,13 @@ def _device_key_to_bluetooth_entity_key( return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/sensorpush/translations/he.json b/homeassistant/components/sensorpush/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/sensorpush/translations/he.json +++ b/homeassistant/components/sensorpush/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/sensorpush/translations/sk.json b/homeassistant/components/sensorpush/translations/sk.json new file mode 100644 index 00000000000..b121bbc35a3 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 1c4b00e25cc..e86ff697896 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.10.0"], + "requirements": ["sentry-sdk==1.11.0"], "codeowners": ["@dcramer", "@frenck"], "integration_type": "service", "iot_class": "cloud_polling" diff --git a/homeassistant/components/sentry/translations/sk.json b/homeassistant/components/sentry/translations/sk.json new file mode 100644 index 00000000000..6be09774065 --- /dev/null +++ b/homeassistant/components/sentry/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "bad_dsn": "Neplatn\u00e9 DSN", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/sk.json b/homeassistant/components/senz/translations/sk.json new file mode 100644 index 00000000000..63bb5647a49 --- /dev/null +++ b/homeassistant/components/senz/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu." + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 09457fc4ca5..cfe834bd648 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.2.0"], + "requirements": ["pillow==9.3.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sharkiq/translations/bg.json b/homeassistant/components/sharkiq/translations/bg.json index 339d2106051..065933b2fb9 100644 --- a/homeassistant/components/sharkiq/translations/bg.json +++ b/homeassistant/components/sharkiq/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/sharkiq/translations/sk.json b/homeassistant/components/sharkiq/translations/sk.json index 71a7aea5018..76f7b6b566c 100644 --- a/homeassistant/components/sharkiq/translations/sk.json +++ b/homeassistant/components/sharkiq/translations/sk.json @@ -1,10 +1,27 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth": { + "data": { + "password": "Heslo" + } + }, + "user": { + "data": { + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index a0dfd3388b4..ac47830f840 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -43,7 +43,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: cache[cmd] = prog, args, args_compiled else: prog, args = cmd.split(" ", 1) - args_compiled = template.Template(args, hass) + args_compiled = template.Template(str(args), hass) cache[cmd] = prog, args, args_compiled if args_compiled: diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b65c314789a..1e77fae439e 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -6,7 +6,7 @@ from typing import Any, Final import aioshelly from aioshelly.block_device import BlockDevice from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError -from aioshelly.rpc_device import RpcDevice +from aioshelly.rpc_device import RpcDevice, UpdateType import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -252,7 +252,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo hass.config_entries.async_setup_platforms(entry, platforms) @callback - def _async_device_online(_: Any) -> None: + def _async_device_online(_: Any, update_type: UpdateType) -> None: LOGGER.debug("Device %s is online, resuming setup", entry.title) shelly_entry_data.device = None diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index cfacdf85cfd..a5265241da3 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -268,10 +268,8 @@ class RestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity): entity_description: RestBinarySensorDescription @property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return true if REST sensor state is on.""" - if self.attribute_value is None: - return None return bool(self.attribute_value) @@ -281,10 +279,8 @@ class RpcBinarySensor(ShellyRpcAttributeEntity, BinarySensorEntity): entity_description: RpcBinarySensorDescription @property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return true if RPC sensor state is on.""" - if self.attribute_value is None: - return None return bool(self.attribute_value) @@ -308,7 +304,7 @@ class RpcSleepingBinarySensor(ShellySleepingRpcAttributeEntity, BinarySensorEnti entity_description: RpcBinarySensorDescription @property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return true if RPC sensor state is on.""" if self.coordinator.device.initialized: return bool(self.attribute_value) diff --git a/homeassistant/components/shelly/bluetooth/__init__.py b/homeassistant/components/shelly/bluetooth/__init__.py new file mode 100644 index 00000000000..429fae1a9a1 --- /dev/null +++ b/homeassistant/components/shelly/bluetooth/__init__.py @@ -0,0 +1,69 @@ +"""Bluetooth support for shelly.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from aioshelly.ble import async_start_scanner +from aioshelly.ble.const import ( + BLE_SCAN_RESULT_EVENT, + BLE_SCAN_RESULT_VERSION, + DEFAULT_DURATION_MS, + DEFAULT_INTERVAL_MS, + DEFAULT_WINDOW_MS, +) + +from homeassistant.components.bluetooth import ( + HaBluetoothConnector, + async_get_advertisement_callback, + async_register_scanner, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback +from homeassistant.helpers.device_registry import format_mac + +from ..const import BLEScannerMode +from .scanner import ShellyBLEScanner + +if TYPE_CHECKING: + from ..coordinator import ShellyRpcCoordinator + + +async def async_connect_scanner( + hass: HomeAssistant, + coordinator: ShellyRpcCoordinator, + scanner_mode: BLEScannerMode, +) -> CALLBACK_TYPE: + """Connect scanner.""" + device = coordinator.device + entry = coordinator.entry + source = format_mac(coordinator.mac).upper() + new_info_callback = async_get_advertisement_callback(hass) + connector = HaBluetoothConnector( + # no active connections to shelly yet + client=None, # type: ignore[arg-type] + source=source, + can_connect=lambda: False, + ) + scanner = ShellyBLEScanner( + hass, source, entry.title, new_info_callback, connector, False + ) + unload_callbacks = [ + async_register_scanner(hass, scanner, False), + scanner.async_setup(), + coordinator.async_subscribe_events(scanner.async_on_event), + ] + await async_start_scanner( + device=device, + active=scanner_mode == BLEScannerMode.ACTIVE, + event_type=BLE_SCAN_RESULT_EVENT, + data_version=BLE_SCAN_RESULT_VERSION, + interval_ms=DEFAULT_INTERVAL_MS, + window_ms=DEFAULT_WINDOW_MS, + duration_ms=DEFAULT_DURATION_MS, + ) + + @hass_callback + def _async_unload() -> None: + for callback in unload_callbacks: + callback() + + return _async_unload diff --git a/homeassistant/components/shelly/bluetooth/scanner.py b/homeassistant/components/shelly/bluetooth/scanner.py new file mode 100644 index 00000000000..f255d01c78b --- /dev/null +++ b/homeassistant/components/shelly/bluetooth/scanner.py @@ -0,0 +1,48 @@ +"""Bluetooth scanner for shelly.""" +from __future__ import annotations + +import logging +from typing import Any + +from aioshelly.ble import parse_ble_scan_result_event +from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT, BLE_SCAN_RESULT_VERSION + +from homeassistant.components.bluetooth import BaseHaRemoteScanner +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + + +class ShellyBLEScanner(BaseHaRemoteScanner): + """Scanner for shelly.""" + + @callback + def async_on_event(self, event: dict[str, Any]) -> None: + """Process an event from the shelly and ignore if its not a ble.scan_result.""" + if event.get("event") != BLE_SCAN_RESULT_EVENT: + return + + data = event["data"] + + if data[0] != BLE_SCAN_RESULT_VERSION: + _LOGGER.warning("Unsupported BLE scan result version: %s", data[0]) + return + + try: + address, rssi, parsed = parse_ble_scan_result_event(data) + except Exception as err: # pylint: disable=broad-except + # Broad exception catch because we have no + # control over the data that is coming in. + _LOGGER.error("Failed to parse BLE event: %s", err, exc_info=True) + return + + self._async_on_advertisement( + address, + rssi, + parsed.local_name, + parsed.service_uuids, + parsed.service_data, + parsed.manufacturer_data, + parsed.tx_power, + {}, + ) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 38ba4a51c9f..a624ba341af 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -16,7 +16,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry, entity_registry @@ -24,10 +24,11 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.unit_conversion import TemperatureConverter +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .const import LOGGER, SHTRV_01_TEMPERATURE_SETTINGS from .coordinator import ShellyBlockCoordinator, get_entry_data -from .utils import get_device_entry_gen async def async_setup_entry( @@ -36,10 +37,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate device.""" - - if get_device_entry_gen(config_entry) == 2: - return - coordinator = get_entry_data(hass)[config_entry.entry_id].block assert coordinator if coordinator.device.initialized: @@ -109,11 +106,11 @@ class BlockSleepingClimate( _attr_icon = "mdi:thermostat" _attr_max_temp = SHTRV_01_TEMPERATURE_SETTINGS["max"] _attr_min_temp = SHTRV_01_TEMPERATURE_SETTINGS["min"] - _attr_supported_features: int = ( + _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] - _attr_temperature_unit = TEMP_CELSIUS + _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__( self, @@ -131,7 +128,14 @@ class BlockSleepingClimate( self.last_state: State | None = None self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] - self._last_target_temp = 20.0 + if coordinator.hass.config.units is US_CUSTOMARY_SYSTEM: + self._last_target_temp = TemperatureConverter.convert( + SHTRV_01_TEMPERATURE_SETTINGS["default"], + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + ) + else: + self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"] if self.block is not None and self.device_block is not None: self._unique_id = f"{self.coordinator.mac}-{self.block.description}" @@ -162,14 +166,32 @@ class BlockSleepingClimate( """Set target temperature.""" if self.block is not None: return cast(float, self.block.targetTemp) - return self.last_state_attributes.get("temperature") + # The restored value can be in Fahrenheit so we have to convert it to Celsius + # because we use this unit internally in integration. + target_temp = self.last_state_attributes.get("temperature") + if self.hass.config.units is US_CUSTOMARY_SYSTEM and target_temp: + return TemperatureConverter.convert( + cast(float, target_temp), + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + ) + return target_temp @property def current_temperature(self) -> float | None: """Return current temperature.""" if self.block is not None: return cast(float, self.block.temp) - return self.last_state_attributes.get("current_temperature") + # The restored value can be in Fahrenheit so we have to convert it to Celsius + # because we use this unit internally in integration. + current_temp = self.last_state_attributes.get("current_temperature") + if self.hass.config.units is US_CUSTOMARY_SYSTEM and current_temp: + return TemperatureConverter.convert( + cast(float, current_temp), + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + ) + return current_temp @property def available(self) -> bool: diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 0f6ae9c9da6..9bf4a6126b0 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -12,16 +12,25 @@ from aioshelly.exceptions import ( InvalidAuthError, ) from aioshelly.rpc_device import RpcDevice +from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import aiohttp_client, selector -from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER +from .const import ( + BLE_MIN_VERSION, + CONF_BLE_SCANNER_MODE, + CONF_SLEEP_PERIOD, + DOMAIN, + LOGGER, + BLEScannerMode, +) +from .coordinator import get_entry_data from .utils import ( get_block_device_name, get_block_device_sleep_period, @@ -37,6 +46,13 @@ from .utils import ( HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) +BLE_SCANNER_OPTIONS = [ + selector.SelectOptionDict(value=BLEScannerMode.DISABLED, label="Disabled"), + selector.SelectOptionDict(value=BLEScannerMode.ACTIVE, label="Active"), + selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), +] + + async def validate_input( hass: HomeAssistant, host: str, @@ -310,3 +326,61 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await aioshelly.common.get_info( aiohttp_client.async_get_clientsession(self.hass), host ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + @classmethod + @callback + def async_supports_options_flow( + cls, config_entry: config_entries.ConfigEntry + ) -> bool: + """Return options flow support for this handler.""" + return config_entry.data.get("gen") == 2 and not config_entry.data.get( + CONF_SLEEP_PERIOD + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle the option flow for shelly.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle options flow.""" + if user_input is not None: + entry_data = get_entry_data(self.hass)[self.config_entry.entry_id] + if user_input[CONF_BLE_SCANNER_MODE] != BLEScannerMode.DISABLED and ( + not entry_data.rpc + or AwesomeVersion(entry_data.rpc.device.version) < BLE_MIN_VERSION + ): + return self.async_abort( + reason="ble_unsupported", + description_placeholders={"ble_min_version": BLE_MIN_VERSION}, + ) + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_BLE_SCANNER_MODE, + default=self.config_entry.options.get( + CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED + ), + ): selector.SelectSelector( + selector.SelectSelectorConfig(options=BLE_SCANNER_OPTIONS), + ), + } + ), + ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 39ca515e5ed..41656bbcd6f 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -5,6 +5,10 @@ from logging import Logger, getLogger import re from typing import Final +from awesomeversion import AwesomeVersion + +from homeassistant.backports.enum import StrEnum + DOMAIN: Final = "shelly" LOGGER: Logger = getLogger(__package__) @@ -143,6 +147,7 @@ SHTRV_01_TEMPERATURE_SETTINGS: Final = { "min": 4, "max": 31, "step": 0.5, + "default": 20.0, } # Kelvin value for colorTemp @@ -156,3 +161,15 @@ UPTIME_DEVIATION: Final = 5 ENTRY_RELOAD_COOLDOWN = 60 SHELLY_GAS_MODELS = ["SHGS-1"] + +BLE_MIN_VERSION = AwesomeVersion("0.12.0-beta2") + +CONF_BLE_SCANNER_MODE = "ble_scanner_mode" + + +class BLEScannerMode(StrEnum): + """BLE scanner mode.""" + + DISABLED = "disabled" + ACTIVE = "active" + PASSIVE = "passive" diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 23f905b0fd9..867cacb3647 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -1,29 +1,35 @@ """Coordinators for the Shelly integration.""" from __future__ import annotations -from collections.abc import Coroutine +import asyncio +from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta from typing import Any, cast import aioshelly +from aioshelly.ble import async_ensure_ble_enabled, async_stop_scanner from aioshelly.block_device import BlockDevice from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError -from aioshelly.rpc_device import RpcDevice +from aioshelly.rpc_device import RpcDevice, UpdateType +from awesomeversion import AwesomeVersion from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .bluetooth import async_connect_scanner from .const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, ATTR_DEVICE, ATTR_GENERATION, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, + BLE_MIN_VERSION, + CONF_BLE_SCANNER_MODE, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DOMAIN, @@ -40,6 +46,7 @@ from .const import ( SHBTN_MODELS, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, + BLEScannerMode, ) from .utils import ( device_update_info, @@ -162,6 +169,7 @@ class ShellyBlockCoordinator(DataUpdateCoordinator): "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids ): + LOGGER.debug("Skipping non-input event block %s", block.description) continue channel = int(block.channel or 0) + 1 @@ -174,6 +182,7 @@ class ShellyBlockCoordinator(DataUpdateCoordinator): or last_event_count == block.inputEventCnt or event_type == "" ): + LOGGER.debug("Skipping block event %s", event_type) continue if event_type in INPUTS_EVENTS_DICT: @@ -187,12 +196,6 @@ class ShellyBlockCoordinator(DataUpdateCoordinator): ATTR_GENERATION: 1, }, ) - else: - LOGGER.warning( - "Shelly input event %s for device %s is not supported, please open issue", - event_type, - self.name, - ) if self._last_cfg_changed is not None and cfg_changed > self._last_cfg_changed: LOGGER.info( @@ -336,7 +339,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): ) self.entry = entry self.device = device + self.connected = False + self._disconnected_callbacks: list[CALLBACK_TYPE] = [] + self._connection_lock = asyncio.Lock() + self._event_listeners: list[Callable[[dict[str, Any]], None]] = [] self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, @@ -345,18 +352,14 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): function=self._async_reload_entry, ) entry.async_on_unload(self._debounced_reload.async_cancel) - - entry.async_on_unload( - self.async_add_listener(self._async_device_updates_handler) - ) - self._last_event: dict[str, Any] | None = None - entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) + entry.async_on_unload(entry.add_update_listener(self._async_update_listener)) async def _async_reload_entry(self) -> None: """Reload entry.""" + self._debounced_reload.async_cancel() LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) @@ -379,25 +382,41 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): return True @callback - def _async_device_updates_handler(self) -> None: - """Handle device updates.""" - if ( - not self.device.initialized - or not self.device.event - or self.device.event == self._last_event - ): - return + def async_subscribe_events( + self, event_callback: Callable[[dict[str, Any]], None] + ) -> CALLBACK_TYPE: + """Subscribe to events.""" - self.update_sleep_period() + def _unsubscribe() -> None: + self._event_listeners.remove(event_callback) - self._last_event = self.device.event + self._event_listeners.append(event_callback) - for event in self.device.event["events"]: + return _unsubscribe + + async def _async_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Reconfigure on update.""" + async with self._connection_lock: + if self.connected: + self._async_run_disconnected_events() + await self._async_run_connected_events() + + @callback + def _async_device_event_handler(self, event_data: dict[str, Any]) -> None: + """Handle device events.""" + events: list[dict[str, Any]] = event_data["events"] + for event in events: event_type = event.get("event") if event_type is None: continue + for event_callback in self._event_listeners: + event_callback(event) + if event_type == "config_changed": + self.update_sleep_period() LOGGER.info( "Config for %s changed, reloading entry in %s seconds", self.name, @@ -453,6 +472,77 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): """Firmware version of the device.""" return self.device.firmware_version if self.device.initialized else "" + async def _async_disconnected(self) -> None: + """Handle device disconnected.""" + async with self._connection_lock: + if not self.connected: # Already disconnected + return + self.connected = False + self._async_run_disconnected_events() + + @callback + def _async_run_disconnected_events(self) -> None: + """Run disconnected events. + + This will be executed on disconnect or when the config entry + is updated. + """ + for disconnected_callback in self._disconnected_callbacks: + disconnected_callback() + self._disconnected_callbacks.clear() + + async def _async_connected(self) -> None: + """Handle device connected.""" + async with self._connection_lock: + if self.connected: # Already connected + return + self.connected = True + await self._async_run_connected_events() + + async def _async_run_connected_events(self) -> None: + """Run connected events. + + This will be executed on connect or when the config entry + is updated. + """ + await self._async_connect_ble_scanner() + + async def _async_connect_ble_scanner(self) -> None: + """Connect BLE scanner.""" + ble_scanner_mode = self.entry.options.get( + CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED + ) + if ble_scanner_mode == BLEScannerMode.DISABLED: + await async_stop_scanner(self.device) + return + if AwesomeVersion(self.device.version) < BLE_MIN_VERSION: + LOGGER.error( + "BLE not supported on device %s with firmware %s; upgrade to %s", + self.name, + self.device.version, + BLE_MIN_VERSION, + ) + return + if await async_ensure_ble_enabled(self.device): + # BLE enable required a reboot, don't bother connecting + # the scanner since it will be disconnected anyway + return + self._disconnected_callbacks.append( + await async_connect_scanner(self.hass, self, ble_scanner_mode) + ) + + @callback + def _async_handle_update(self, device_: RpcDevice, update_type: UpdateType) -> None: + """Handle device update.""" + if update_type is UpdateType.INITIALIZED: + self.hass.async_create_task(self._async_connected()) + elif update_type is UpdateType.DISCONNECTED: + self.hass.async_create_task(self._async_disconnected()) + elif update_type is UpdateType.STATUS: + self.async_set_updated_data(self.device) + elif update_type is UpdateType.EVENT and (event := self.device.event): + self._async_device_event_handler(event) + def async_setup(self) -> None: """Set up the coordinator.""" dev_reg = device_registry.async_get(self.hass) @@ -467,11 +557,17 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): configuration_url=f"http://{self.entry.data[CONF_HOST]}", ) self.device_id = entry.id - self.device.subscribe_updates(self.async_set_updated_data) + self.device.subscribe_updates(self._async_handle_update) + if self.device.initialized: + # If we are already initialized, we are connected + self.hass.async_create_task(self._async_connected()) async def shutdown(self) -> None: """Shutdown the coordinator.""" + if self.device.connected: + await async_stop_scanner(self.device) await self.device.shutdown() + await self._async_disconnected() async def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" @@ -511,11 +607,6 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator): except InvalidAuthError: self.entry.async_start_reauth(self.hass) - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - @property def mac(self) -> str: """Mac address of the device.""" @@ -526,9 +617,6 @@ def get_block_coordinator_by_device_id( hass: HomeAssistant, device_id: str ) -> ShellyBlockCoordinator | None: """Get a Shelly block device coordinator for the given device id.""" - if not hass.data.get(DOMAIN): - return None - dev_reg = device_registry.async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: @@ -545,9 +633,6 @@ def get_rpc_coordinator_by_device_id( hass: HomeAssistant, device_id: str ) -> ShellyRpcCoordinator | None: """Get a Shelly RPC device coordinator for the given device id.""" - if not hass.data.get(DOMAIN): - return None - dev_reg = device_registry.async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 66b95a7a7fd..f2020597277 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -75,7 +75,7 @@ class BlockShellyCover(ShellyBlockEntity, CoverEntity): """Initialize block cover.""" super().__init__(coordinator, block) self.control_result: dict[str, Any] | None = None - self._attr_supported_features: int = ( + self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP ) if self.coordinator.device.settings["rollers"][0]["positioning"]: @@ -151,7 +151,7 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity): """Initialize rpc cover.""" super().__init__(coordinator, f"cover:{id_}") self._id = id_ - self._attr_supported_features: int = ( + self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP ) if self.status["pos_control"]: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 96f566f6a2e..3c57bee8f02 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -21,12 +21,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SLEEP_PERIOD, LOGGER -from .coordinator import ( - ShellyBlockCoordinator, - ShellyRpcCoordinator, - ShellyRpcPollingCoordinator, - get_entry_data, -) +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .utils import ( async_remove_shelly_entity, get_block_entity_name, @@ -269,21 +264,10 @@ def async_setup_entry_rest( """Set up entities for REST sensors.""" coordinator = get_entry_data(hass)[config_entry.entry_id].rest assert coordinator - entities = [] - for sensor_id in sensors: - description = sensors.get(sensor_id) - - if not coordinator.device.settings.get("sleep_mode"): - entities.append((sensor_id, description)) - - if not entities: - return async_add_entities( - [ - sensor_class(coordinator, sensor_id, description) - for sensor_id, description in entities - ] + sensor_class(coordinator, sensor_id, sensors[sensor_id]) + for sensor_id in sensors ) @@ -350,10 +334,6 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """When entity is added to HASS.""" self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) - async def async_update(self) -> None: - """Update entity with latest info.""" - await self.coordinator.async_request_refresh() - @callback def _update_callback(self) -> None: """Handle device update.""" @@ -373,16 +353,12 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): self.coordinator.entry.async_start_reauth(self.hass) -class ShellyRpcEntity(entity.Entity): +class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): """Helper class to represent a rpc entity.""" - def __init__( - self, - coordinator: ShellyRpcCoordinator | ShellyRpcPollingCoordinator, - key: str, - ) -> None: + def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: """Initialize Shelly entity.""" - self.coordinator = coordinator + super().__init__(coordinator) self.key = key self._attr_should_poll = False self._attr_device_info = { @@ -405,10 +381,6 @@ class ShellyRpcEntity(entity.Entity): """When entity is added to HASS.""" self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) - async def async_update(self) -> None: - """Update entity with latest info.""" - await self.coordinator.async_request_refresh() - @callback def _update_callback(self) -> None: """Handle device update.""" @@ -525,16 +497,6 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): ) return self._last_value - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes.""" - if self.entity_description.extra_state_attributes is None: - return None - - return self.entity_description.extra_state_attributes( - self.block_coordinator.device.status - ) - class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): """Helper class to represent a rpc attribute.""" @@ -586,19 +548,6 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): self.coordinator.device.status[self.key][self.entity_description.sub_key] ) - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes.""" - if self.entity_description.extra_state_attributes is None: - return None - - assert self.coordinator.device.shelly - - return self.entity_description.extra_state_attributes( - self.coordinator.device.status[self.key][self.entity_description.sub_key], - self.coordinator.device.shelly, - ) - class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEntity): """Represent a shelly sleeping block attribute entity.""" diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index dda9a41bb89..805b8147ba5 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -184,16 +184,17 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" + brightness_pct: int if self.mode == "color": if self.control_result: brightness_pct = self.control_result["gain"] else: - brightness_pct = self.block.gain + brightness_pct = cast(int, self.block.gain) else: if self.control_result: brightness_pct = self.control_result["brightness"] else: - brightness_pct = self.block.brightness + brightness_pct = cast(int, self.block.brightness) return round(255 * brightness_pct / 100) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 70970e73e30..7c2fe3beef6 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,16 +3,17 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==4.1.2"], - "dependencies": ["http"], + "requirements": ["aioshelly==5.1.0"], + "dependencies": ["bluetooth", "http"], "zeroconf": [ { "type": "_http._tcp.local.", "name": "shelly*" } ], - "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"], + "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74", "@bdraco"], "iot_class": "local_push", "loggers": ["aioshelly"], - "integration_type": "device" + "integration_type": "device", + "quality_scale": "platinum" } diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index bb7f17ea18d..7066f386355 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -25,7 +25,6 @@ from .entity import ( ShellySleepingBlockAttributeEntity, async_setup_entry_attribute_entities, ) -from .utils import get_device_entry_gen @dataclass @@ -77,9 +76,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up numbers for device.""" - if get_device_entry_gen(config_entry) == 2: - return - if config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_attribute_entities( hass, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7d99f015c5e..6bc3b52b65f 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -417,7 +417,7 @@ RPC_SENSORS: Final = { available=lambda status: status is not None, ), "analoginput": RpcSensorDescription( - key="analoginput", + key="input", sub_key="percent", name="Analog Input", native_unit_of_measurement=PERCENTAGE, diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index d3684f85be2..15f3be4d1e5 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -58,5 +58,18 @@ "double_push": "{subtype} double push", "long_push": "{subtype} long push" } + }, + "options": { + "step": { + "init": { + "description": "Bluetooth scanning can be active or passive. With active, the Shelly requests data from nearby devices; with passive, the Shelly receives unsolicited data from nearby devices.", + "data": { + "ble_scanner_mode": "Bluetooth scanner mode" + } + } + }, + "abort": { + "ble_unsupported": "Bluetooth support requires firmware version {ble_min_version} or newer." + } } } diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index 1cdcd4e5d86..8e3487284ef 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "unsupported_firmware": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0444\u044a\u0440\u043c\u0443\u0435\u0440\u0430." }, "error": { diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 4dd4626b6dc..73736725969 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -58,5 +58,17 @@ "single_push": "{subtype} clicat una vegada", "triple": "{subtype} clicat tres vegades" } + }, + "options": { + "abort": { + "ble_unsupported": "La compatibilitat amb Bluetooth necessita la versi\u00f3 de microprogramari {ble_min_version} o una de m\u00e9s recent." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Mode d'escaneig Bluetooth" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json index c2f45d0f4c7..248a101a5b3 100644 --- a/homeassistant/components/shelly/translations/cs.json +++ b/homeassistant/components/shelly/translations/cs.json @@ -57,5 +57,18 @@ "single_push": "\"{subtype}\" stisknuto jednou", "triple": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t" } + }, + "options": { + "abort": { + "ble_unsupported": "Podpora Bluetooth vy\u017eaduje verzi firmwaru {ble_min_version} nebo nov\u011bj\u0161\u00ed." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Re\u017eim skenov\u00e1n\u00ed Bluetooth" + }, + "description": "Bluetooth skenov\u00e1n\u00ed m\u016f\u017ee b\u00fdt aktivn\u00ed nebo pasivn\u00ed. Kdy\u017e je aktivn\u00ed, Shelly po\u017eaduje data z okoln\u00edch za\u0159\u00edzen\u00ed; s pasivn\u00ed, Shelly p\u0159ij\u00edm\u00e1 nevy\u017e\u00e1dan\u00e1 data z okoln\u00edch za\u0159\u00edzen\u00ed." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 4990f40a93a..c0cc533d92d 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -58,5 +58,18 @@ "single_push": "{subtype} einfacher Druck", "triple": "{subtype} dreifach bet\u00e4tigt" } + }, + "options": { + "abort": { + "ble_unsupported": "Bluetooth-Unterst\u00fctzung erfordert Firmware-Version {ble_min_version} oder neuer." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth Scannermodus" + }, + "description": "Bluetooth-Scannen kann aktiv oder passiv sein. Bei aktiv fordert Shelly Daten von Ger\u00e4ten in der N\u00e4he an; Mit Passiv empf\u00e4ngt Shelly unaufgefordert Daten von Ger\u00e4ten in der N\u00e4he." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/el.json b/homeassistant/components/shelly/translations/el.json index 01c7af19be0..f1cb15a3805 100644 --- a/homeassistant/components/shelly/translations/el.json +++ b/homeassistant/components/shelly/translations/el.json @@ -58,5 +58,18 @@ "single_push": "{subtype} \u03bc\u03bf\u03bd\u03ae \u03ce\u03b8\u03b7\u03c3\u03b7", "triple": "\u03a4\u03c1\u03b9\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf {subtype}" } + }, + "options": { + "abort": { + "ble_unsupported": "\u0397 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 Bluetooth \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd {ble_min_version} \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03b1\u03c1\u03c9\u03c4\u03ae Bluetooth" + }, + "description": "\u0397 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 Bluetooth \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae \u03ae \u03c0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae. \u039c\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae, \u03c4\u03bf Shelly \u03b6\u03b7\u03c4\u03ac \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03c0\u03cc \u03ba\u03bf\u03bd\u03c4\u03b9\u03bd\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2- \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae, \u03c4\u03bf Shelly \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7 \u03b6\u03b7\u03c4\u03b7\u03b8\u03ad\u03bd\u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03c0\u03cc \u03ba\u03bf\u03bd\u03c4\u03b9\u03bd\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index dfc2e8aebec..4ca783b6fe4 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -58,5 +58,18 @@ "single_push": "{subtype} single push", "triple": "{subtype} triple clicked" } + }, + "options": { + "abort": { + "ble_unsupported": "Bluetooth support requires firmware version {ble_min_version} or newer." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth scanner mode" + }, + "description": "Bluetooth scanning can be active or passive. With active, the Shelly requests data from nearby devices; with passive, the Shelly receives unsolicited data from nearby devices." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 7acd73471b7..2bbf32ebe86 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -58,5 +58,18 @@ "single_push": "Pulsaci\u00f3n simple de {subtype}", "triple": "Pulsaci\u00f3n triple de {subtype}" } + }, + "options": { + "abort": { + "ble_unsupported": "La compatibilidad con Bluetooth requiere la versi\u00f3n de firmware {ble_min_version} o m\u00e1s reciente." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Modo de esc\u00e1ner Bluetooth" + }, + "description": "El escaneo de Bluetooth puede ser activo o pasivo. Con activo, Shelly solicita datos de dispositivos cercanos; con pasivo, Shelly recibe datos no solicitados de dispositivos cercanos." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index bc68caeb9bb..69889526619 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -58,5 +58,18 @@ "single_push": "{subtype} l\u00fchike vajutus", "triple": "Nuppu {subtype} kl\u00f5psati kolm korda" } + }, + "options": { + "abort": { + "ble_unsupported": "Bluetoothi tugi n\u00f5uab p\u00fcsivara versiooni {ble_min_version} v\u00f5i uuemat." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth-sk\u00e4nneri re\u017eiim" + }, + "description": "Bluetoothi skannimine v\u00f5ib olla aktiivne v\u00f5i passiivne. Aktiivse oleku korral k\u00fcsib Shelly andmeid l\u00e4hedalasuvatest seadmetest, passiivse puhul saab Shelly l\u00e4hedalasuvatest seadmetest soovimatuid andmeid." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 7878e7d28f8..8b7ebcac15e 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -58,5 +58,18 @@ "single_push": "{subtype} egy lenyom\u00e1s", "triple": "{subtype} tripla kattint\u00e1s" } + }, + "options": { + "abort": { + "ble_unsupported": "A Bluetooth-t\u00e1mogat\u00e1shoz legal\u00e1bb {ble_min_version} firmware verzi\u00f3 sz\u00fcks\u00e9ges." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth szkenner m\u00f3d" + }, + "description": "A Bluetooth-keres\u00e9s lehet akt\u00edv vagy passz\u00edv. Ha akt\u00edv, a Shelly adatokat k\u00e9r a k\u00f6zeli eszk\u00f6z\u00f6kr\u0151l; a passz\u00edv funkci\u00f3val a Shelly k\u00e9retlen adatokat is fogad a k\u00f6zeli eszk\u00f6z\u00f6kr\u0151l." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json index 104494e01c6..ca945eff968 100644 --- a/homeassistant/components/shelly/translations/id.json +++ b/homeassistant/components/shelly/translations/id.json @@ -58,5 +58,18 @@ "single_push": "Push tunggal {subtype}", "triple": "{subtype} diklik tiga kali" } + }, + "options": { + "abort": { + "ble_unsupported": "Dukungan Bluetooth memerlukan versi firmware {ble_min_version} atau yang lebih baru." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Mode pemindai Bluetooth" + }, + "description": "Mode pemindaian Bluetooth bisa berupa mode aktif atau pasif. Pada mode aktif, Shelly akan meminta data dari perangkat di dekatnya; pada mode pasif, Shelly akan menerima data yang tidak diminta dari perangkat di dekatnya." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index 7b882246e66..04bf99179b4 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -58,5 +58,18 @@ "single_push": "{subtype} singola pressione", "triple": "{subtype} premuto tre volte" } + }, + "options": { + "abort": { + "ble_unsupported": "Il supporto Bluetooth richiede la versione del firmware {ble_min_version} o successiva." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Modalit\u00e0 scansione Bluetooth" + }, + "description": "La scansione Bluetooth pu\u00f2 essere attiva o passiva. In caso di scansione attiva, lo Shelly richiede dati ai dispositivi vicini; in caso di scansione passiva, lo Shelly riceve dati non richiesti dai dispositivi vicini." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index 70ffbad804b..4d2fa88822b 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -56,5 +56,18 @@ "single_push": "{subtype} een druk", "triple": "{subtype} driemaal geklikt" } + }, + "options": { + "abort": { + "ble_unsupported": "Bluetooth ondersteuning vereist firmwareversie {ble_min_version} of later." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth scannermodus" + }, + "description": "Het scannen van Bluetooth kan actief of passief worden ingesteld. In de actieve modus zal Shelly de gegevens opvragen van apparaten van dichtbij. In de passieve modus zal Shelly ongevraagd gegevens van apparaten dichtbij opvragen." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index 2f483843e52..4b1390b4dfe 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -58,5 +58,18 @@ "single_push": "{subtype} enkelt trykk", "triple": "{subtype} trippelklikket" } + }, + "options": { + "abort": { + "ble_unsupported": "Bluetooth-st\u00f8tte krever fastvareversjon {ble_min_version} eller nyere." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth-skannermodus" + }, + "description": "Bluetooth-skanning kan v\u00e6re aktiv eller passiv. Med aktiv ber Shelly om data fra enheter i n\u00e6rheten; med passiv, mottar Shelly u\u00f8nsket data fra enheter i n\u00e6rheten." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pt-BR.json b/homeassistant/components/shelly/translations/pt-BR.json index 0a546c9807d..508552a92f4 100644 --- a/homeassistant/components/shelly/translations/pt-BR.json +++ b/homeassistant/components/shelly/translations/pt-BR.json @@ -58,5 +58,18 @@ "single_push": "{subtype} \u00fanico empurr\u00e3o", "triple": "{subtype} triplo clicado" } + }, + "options": { + "abort": { + "ble_unsupported": "O suporte a Bluetooth requer a vers\u00e3o de firmware {ble_min_version} ou mais recente." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Modo de varredura Bluetooth" + }, + "description": "A varredura Bluetooth pode ser ativa ou passiva. Com ativo, o Shelly solicita dados de dispositivos pr\u00f3ximos; com passivo, o Shelly recebe dados n\u00e3o solicitados de dispositivos pr\u00f3ximos." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 31b2a8ca9a1..30f28100fa9 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -58,5 +58,18 @@ "single_push": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", "triple": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } + }, + "options": { + "abort": { + "ble_unsupported": "\u0414\u043b\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 Bluetooth \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0430 \u0432\u0435\u0440\u0441\u0438\u0438 {ble_min_version} \u0438\u043b\u0438 \u043d\u043e\u0432\u0435\u0435." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "\u0420\u0435\u0436\u0438\u043c \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Bluetooth" + }, + "description": "\u0421\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Bluetooth \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0438\u043b\u0438 \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u043c. \u041f\u0440\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 Shelly \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443 \u0431\u043b\u0438\u0437\u043b\u0435\u0436\u0430\u0449\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432; \u043f\u0440\u0438 \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u043c Shelly \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043d\u0435\u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0442 \u0431\u043b\u0438\u0437\u043b\u0435\u0436\u0430\u0449\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sk.json b/homeassistant/components/shelly/translations/sk.json index a019d22d264..1a36257a3a5 100644 --- a/homeassistant/components/shelly/translations/sk.json +++ b/homeassistant/components/shelly/translations/sk.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", + "firmware_not_fully_provisioned": "Zariadenie nie je plne zabezpe\u010den\u00e9. Kontaktujte podporu spolo\u010dnosti Shelly", "invalid_auth": "Neplatn\u00e9 overenie", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, @@ -20,7 +21,15 @@ "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } }, + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { + "data": { + "host": "Hostite\u013e" + }, "description": "Pred nastaven\u00edm musia by\u0165 zariadenia nap\u00e1jan\u00e9 z bat\u00e9rie zobuden\u00e9. Zobu\u010fte zariadenie pomocou tla\u010didla na \u0148om." } } @@ -46,5 +55,10 @@ "single_push": "{subtype} stla\u010den\u00e9 raz", "triple": "{subtype} stla\u010den\u00e9 trikr\u00e1t" } + }, + "options": { + "abort": { + "ble_unsupported": "Podpora Bluetooth vy\u017eaduje verziu firmv\u00e9ru {ble_min_version} alebo nov\u0161iu." + } } } \ 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 728fcdccbfe..e3165f04a09 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -58,5 +58,18 @@ "single_push": "{subtype} \u55ae\u6309", "triple": "{subtype} \u4e09\u9023\u64ca" } + }, + "options": { + "abort": { + "ble_unsupported": "\u85cd\u82bd\u652f\u63f4\u9700\u8981\u97cc\u9ad4 {ble_min_version} \u7248\u6216\u66f4\u65b0\u7248\u672c\u3002" + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "\u85cd\u82bd\u6383\u7784\u5668\u6a21\u5f0f" + }, + "description": "\u85cd\u82bd\u6383\u63cf\u53ef\u4ee5\u70ba\u4e3b\u52d5\u6216\u88ab\u52d5\u6a21\u5f0f\u3002\u4e3b\u52d5\u6a21\u5f0f\u4e0b\u3001Shelly \u6703\u5411\u5468\u570d\u7684\u88dd\u7f6e\u8acb\u6c42\u8cc7\u6599\uff1b\u88ab\u52d5\u6a21\u5f0f\u4e0b\u3001Shelly \u6703\u7531\u5468\u570d\u7684\u88dd\u7f6e\u63a5\u6536\u5ee3\u64ad\u8cc7\u6599\u3002" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index bf242d47e6c..e13395999b1 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -298,7 +298,7 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str: entity_name = device.config[key].get("name", device_name) if entity_name is None: - if [k for k in key if k.startswith(("input", "switch"))]: + if key.startswith(("input:", "switch:")): return f"{device_name} {key.replace(':', '_')}" return device_name diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 0a2a17db200..25d3f447a09 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -90,7 +90,6 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity): """Class for SIA Alarm Control Panels.""" entity_description: SIAAlarmControlPanelEntityDescription - _attr_supported_features = 0 def __init__( self, diff --git a/homeassistant/components/sia/translations/cs.json b/homeassistant/components/sia/translations/cs.json index 7940c6378fe..1ff2a3eb539 100644 --- a/homeassistant/components/sia/translations/cs.json +++ b/homeassistant/components/sia/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_ping": "Interval pingu mus\u00ed b\u00fdt mezi 1 a 1440 minutami.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/sia/translations/sk.json b/homeassistant/components/sia/translations/sk.json index d703643c7de..4f1a4b7b38a 100644 --- a/homeassistant/components/sia/translations/sk.json +++ b/homeassistant/components/sia/translations/sk.json @@ -1,8 +1,25 @@ { "config": { + "error": { + "invalid_account_format": "\u00da\u010det nie je hexadecim\u00e1lna hodnota, pou\u017e\u00edvajte len 0-9 a A-F.", + "invalid_account_length": "\u00da\u010det nem\u00e1 spr\u00e1vnu d\u013a\u017eku, mus\u00ed ma\u0165 od 3 do 16 znakov.", + "invalid_key_format": "K\u013e\u00fa\u010d nie je hexadecim\u00e1lna hodnota, pou\u017e\u00edvajte len 0-9 a A-F.", + "invalid_key_length": "K\u013e\u00fa\u010d nem\u00e1 spr\u00e1vnu d\u013a\u017eku, mus\u00ed ma\u0165 16, 24 alebo 32 hexadecim\u00e1lnych znakov.", + "invalid_ping": "Interval pingu mus\u00ed by\u0165 medzi 1 a 1440 min\u00fatami.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { + "additional_account": { + "data": { + "account": "ID \u00fa\u010dtu", + "ping_interval": "Ping Interval (min)" + }, + "title": "Pridajte \u010fal\u0161\u00ed \u00fa\u010det k aktu\u00e1lnemu portu." + }, "user": { "data": { + "account": "ID \u00fa\u010dtu", + "ping_interval": "Ping Interval (min)", "port": "Port", "protocol": "Protokol" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 400664079e2..042a642429f 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.2.0", "simplehound==0.3"], + "requirements": ["pillow==9.3.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling", "loggers": ["simplehound"] diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json index 4252be0764e..2527b2a4329 100644 --- a/homeassistant/components/simplepush/translations/ca.json +++ b/homeassistant/components/simplepush/translations/ca.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "La configuraci\u00f3 YAML de Simplepush est\u00e0 sent eliminada" - }, "removed_yaml": { "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Simplepush s'ha eliminat" diff --git a/homeassistant/components/simplepush/translations/de.json b/homeassistant/components/simplepush/translations/de.json index 16e916f6d2f..86db336b042 100644 --- a/homeassistant/components/simplepush/translations/de.json +++ b/homeassistant/components/simplepush/translations/de.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Die Konfiguration von Simplepush mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Simplepush YAML-Konfiguration wird entfernt" - }, "removed_yaml": { "description": "Die Konfiguration von Simplepush mittels YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird vom Home Assistant nicht verwendet.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", "title": "Die Simplepush YAML-Konfiguration wurde entfernt" diff --git a/homeassistant/components/simplepush/translations/el.json b/homeassistant/components/simplepush/translations/el.json index 13a961c6aff..9c11e2ea32f 100644 --- a/homeassistant/components/simplepush/translations/el.json +++ b/homeassistant/components/simplepush/translations/el.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - }, "removed_yaml": { "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index cb294813b88..205d3549a52 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Simplepush YAML configuration is being removed" - }, "removed_yaml": { "description": "Configuring Simplepush using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Simplepush YAML configuration has been removed" diff --git a/homeassistant/components/simplepush/translations/es.json b/homeassistant/components/simplepush/translations/es.json index e1bb97650ff..df24d479813 100644 --- a/homeassistant/components/simplepush/translations/es.json +++ b/homeassistant/components/simplepush/translations/es.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de Simplepush mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la configuraci\u00f3n YAML de Simplepush" - }, "removed_yaml": { "description": "Se ha eliminado la configuraci\u00f3n de Simplepush usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Simplepush" diff --git a/homeassistant/components/simplepush/translations/et.json b/homeassistant/components/simplepush/translations/et.json index 7cae6c69edf..8b4e2967d58 100644 --- a/homeassistant/components/simplepush/translations/et.json +++ b/homeassistant/components/simplepush/translations/et.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Simplepushi konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemaldage Simplepushi YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", - "title": "Simplepush YAML-i konfiguratsioon eemaldatakse" - }, "removed_yaml": { "description": "Simplepushi konfigureerimine YAMLi abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nEemaldage Simplepushi YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", "title": "Simplepush YAML-i konfiguratsioon on eemaldatud" diff --git a/homeassistant/components/simplepush/translations/fr.json b/homeassistant/components/simplepush/translations/fr.json index 508d31437cb..4e2493c9359 100644 --- a/homeassistant/components/simplepush/translations/fr.json +++ b/homeassistant/components/simplepush/translations/fr.json @@ -19,9 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "title": "La configuration YAML pour Simplepush sera bient\u00f4t supprim\u00e9e" - }, "removed_yaml": { "title": "La configuration YAML pour Simplepush a \u00e9t\u00e9 supprim\u00e9e" } diff --git a/homeassistant/components/simplepush/translations/hu.json b/homeassistant/components/simplepush/translations/hu.json index b5809898fb1..e8583071f34 100644 --- a/homeassistant/components/simplepush/translations/hu.json +++ b/homeassistant/components/simplepush/translations/hu.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "A Simplepush YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Simplepush YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - }, "removed_yaml": { "description": "A Simplepush YAML-ban t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "A Simplepush YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" diff --git a/homeassistant/components/simplepush/translations/id.json b/homeassistant/components/simplepush/translations/id.json index 954e025cf89..92cbfead8df 100644 --- a/homeassistant/components/simplepush/translations/id.json +++ b/homeassistant/components/simplepush/translations/id.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Proses konfigurasi Simplepush lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Simplepush dalam proses penghapusan" - }, "removed_yaml": { "description": "Proses konfigurasi Integrasi Simplepush lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", "title": "Konfigurasi YAML Integrasi Simplepush telah dihapus" diff --git a/homeassistant/components/simplepush/translations/it.json b/homeassistant/components/simplepush/translations/it.json index be311f7e0c3..cad83f3670f 100644 --- a/homeassistant/components/simplepush/translations/it.json +++ b/homeassistant/components/simplepush/translations/it.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "La configurazione di Simplepush tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Simplepush sar\u00e0 rimossa" - }, "removed_yaml": { "description": "La configurazione di Simplepush tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Simplepush \u00e8 stata rimossa" diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index 5c4da266036..1c0b745829c 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u306a\u304a\u3001\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - }, "removed_yaml": { "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 8916c7db473..176318b3f3c 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -13,10 +13,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "title": "De Simplepush YAML-configuratie wordt verwijderd" - } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json index 453632f348e..5e71c76bd6d 100644 --- a/homeassistant/components/simplepush/translations/no.json +++ b/homeassistant/components/simplepush/translations/no.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "Simplepush YAML-konfigurasjonen blir fjernet" - }, "removed_yaml": { "description": "Konfigurering av Simplepush med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", "title": "Simplepush YAML-konfigurasjonen er fjernet" diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json index a3269fda96c..39e8e4aaca9 100644 --- a/homeassistant/components/simplepush/translations/pl.json +++ b/homeassistant/components/simplepush/translations/pl.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Konfiguracja Simplepush przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Konfiguracja YAML dla Simplepush zostanie usuni\u0119ta" - }, "removed_yaml": { "description": "Konfiguracja Simplepush za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", "title": "Konfiguracja YAML dla Simplepush zosta\u0142a usuni\u0119ta" diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json index 993c8d7214b..faf55738e88 100644 --- a/homeassistant/components/simplepush/translations/pt-BR.json +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o do Simplepush usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Simplepush YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o Simplepush YAML est\u00e1 sendo removida" - }, "removed_yaml": { "description": "A configura\u00e7\u00e3o do Simplepush usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do Simplepush do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A configura\u00e7\u00e3o Simplepush YAML foi removida" diff --git a/homeassistant/components/simplepush/translations/pt.json b/homeassistant/components/simplepush/translations/pt.json index 67316f99a70..d7e598b33e4 100644 --- a/homeassistant/components/simplepush/translations/pt.json +++ b/homeassistant/components/simplepush/translations/pt.json @@ -13,11 +13,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "A configura\u00e7\u00e3o do Simplepush usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Simplepush YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A configura\u00e7\u00e3o Simplepush YAML est\u00e1 sendo removida" - } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index be00e5deb89..63ec7dabd9f 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - }, "removed_yaml": { "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Simplepush\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" diff --git a/homeassistant/components/simplepush/translations/sk.json b/homeassistant/components/simplepush/translations/sk.json new file mode 100644 index 00000000000..6d26a95a166 --- /dev/null +++ b/homeassistant/components/simplepush/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/sv.json b/homeassistant/components/simplepush/translations/sv.json index 2572b2cce75..bedf1087178 100644 --- a/homeassistant/components/simplepush/translations/sv.json +++ b/homeassistant/components/simplepush/translations/sv.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Konfigurering av Simplepush med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Simplepush YAML-konfigurationen tas bort" - }, "removed_yaml": { "description": "Konfigurering av Simplepush med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", "title": "Simplepush YAML-konfigurationen har tagits bort" diff --git a/homeassistant/components/simplepush/translations/tr.json b/homeassistant/components/simplepush/translations/tr.json index 0a3183d7c90..f8aa2e05c0c 100644 --- a/homeassistant/components/simplepush/translations/tr.json +++ b/homeassistant/components/simplepush/translations/tr.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Simplepush'un YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Simplepush YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "Simplepush YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" - }, "removed_yaml": { "description": "Simplepush'u YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n Simplepush YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", "title": "Simplepush YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json index ea51e58e648..67c70cf4d17 100644 --- a/homeassistant/components/simplepush/translations/zh-Hant.json +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -19,10 +19,6 @@ } }, "issues": { - "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Simplepush YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" - }, "removed_yaml": { "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Simplepush \u7684\u529f\u80fd\u5df2\u7d93\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Simplepush YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 608abb5effb..7b431368328 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -139,24 +139,16 @@ VOLUME_MAP = { "off": Volume.OFF, } -SERVICE_NAME_CLEAR_NOTIFICATIONS = "clear_notifications" SERVICE_NAME_REMOVE_PIN = "remove_pin" SERVICE_NAME_SET_PIN = "set_pin" SERVICE_NAME_SET_SYSTEM_PROPERTIES = "set_system_properties" SERVICES = ( - SERVICE_NAME_CLEAR_NOTIFICATIONS, SERVICE_NAME_REMOVE_PIN, SERVICE_NAME_SET_PIN, SERVICE_NAME_SET_SYSTEM_PROPERTIES, ) -SERVICE_CLEAR_NOTIFICATIONS_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - }, -) - SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( { vol.Required(ATTR_DEVICE_ID): cv.string, @@ -245,11 +237,12 @@ def _async_get_system_for_service_call( ) is None: raise ValueError("No base station registered for alarm control panel") - [system_id] = [ + [system_id_str] = [ identity[1] for identity in base_station_device_entry.identifiers if identity[0] == DOMAIN ] + system_id = int(system_id_str) for entry_id in base_station_device_entry.config_entries: if (simplisafe := hass.data[DOMAIN].get(entry_id)) is None: @@ -304,7 +297,8 @@ def _async_register_base_station( ) -> None: """Register a new bridge.""" device_registry = dr.async_get(hass) - device_registry.async_get_or_create( + + base_station = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, str(system.system_id))}, manufacturer="SimpliSafe", @@ -312,6 +306,20 @@ def _async_register_base_station( name=system.address, ) + # Check for an old system ID format and remove it: + if old_base_station := device_registry.async_get_device( + {(DOMAIN, system.system_id)} # type: ignore[arg-type] + ): + # Update the new base station with any properties the user might have configured + # on the old base station: + device_registry.async_update_device( + base_station.id, + area_id=old_base_station.area_id, + disabled_by=old_base_station.disabled_by, + name_by_user=old_base_station.name_by_user, + ) + device_registry.async_remove_device(old_base_station.id) + @callback def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -384,19 +392,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @_verify_domain_control - @extract_system - async def async_clear_notifications(call: ServiceCall, system: SystemType) -> None: - """Clear all active notifications.""" - _async_log_deprecated_service_call( - hass, - call, - "button.press", - "button.alarm_control_panel_clear_notifications", - "2022.12.0", - ) - await system.async_clear_notifications() - @_verify_domain_control @extract_system async def async_remove_pin(call: ServiceCall, system: SystemType) -> None: @@ -423,11 +418,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service, method, schema in ( - ( - SERVICE_NAME_CLEAR_NOTIFICATIONS, - async_clear_notifications, - SERVICE_CLEAR_NOTIFICATIONS_SCHEMA, - ), (SERVICE_NAME_REMOVE_PIN, async_remove_pin, SERVICE_REMOVE_PIN_SCHEMA), (SERVICE_NAME_SET_PIN, async_set_pin, SERVICE_SET_PIN_SCHEMA), ( diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 5ccbfb96afb..054bf0b702b 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -78,7 +78,7 @@ class TriggeredBinarySensor(SimpliSafeEntity, BinarySensorEntity): simplisafe: SimpliSafe, system: SystemV3, sensor: SensorV3, - device_class: str, + device_class: BinarySensorDeviceClass, ) -> None: """Initialize.""" super().__init__(simplisafe, system, device=sensor) diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 6f9cedc77cb..8aeefcf7846 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -10,7 +10,8 @@ remove_pin: selector: device: integration: simplisafe - model: alarm_control_panel + entity: + domain: alarm_control_panel label_or_pin: name: Label/PIN description: The label/value to remove. @@ -29,7 +30,8 @@ set_pin: selector: device: integration: simplisafe - model: alarm_control_panel + entity: + domain: alarm_control_panel label: name: Label description: The label of the PIN @@ -55,7 +57,8 @@ set_system_properties: selector: device: integration: simplisafe - model: alarm_control_panel + entity: + domain: alarm_control_panel alarm_duration: name: Alarm duration description: The length of a triggered alarm diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index fa71285d6c9..dddb8e8ab5d 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -1,33 +1,17 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "identifier_exists": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, - "progress": { - "email_2fa": "\u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u0442\u0430 \u0441\u0438 \u043f\u043e\u0449\u0430 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043e\u0442 Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - }, - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" - }, - "sms_2fa": { - "data": { - "code": "\u041a\u043e\u0434" - } - }, "user": { "data": { - "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 159b46eeefb..476453cd5b4 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas.", - "email_2fa_timed_out": "S'ha esgotat el temps d'espera de l'autenticaci\u00f3 de dos factors a trav\u00e9s de correu electr\u00f2nic.", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "wrong_account": "Les credencials d'usuari proporcionades no coincideixen amb les d'aquest compte SimpliSafe." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "Els codis d'autoritzaci\u00f3 de SimpliSafe tenen una longitud de 45 car\u00e0cters", "unknown": "Error inesperat" }, - "progress": { - "email_2fa": "Mira el correu electr\u00f2nic on hauries de trobar l'enlla\u00e7 de verificaci\u00f3 de Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Contrasenya" - }, - "description": "Torna a introduir la contrasenya de {username}", - "title": "Reautenticaci\u00f3 de la integraci\u00f3" - }, - "sms_2fa": { - "data": { - "code": "Codi" - }, - "description": "Introdueix el codi d'autenticaci\u00f3 de dos factors que s'ha enviat per SMS." - }, "user": { "data": { - "auth_code": "Codi d'autoritzaci\u00f3", - "password": "Contrasenya", - "username": "Nom d'usuari" + "auth_code": "Codi d'autoritzaci\u00f3" }, "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Si ja has iniciat sessi\u00f3 a SimpliSafe a trav\u00e9s del navegador, potser hauries d'obrir una nova pestanya i copiar-hi l'URL de dalt.\n\nQuan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL `com.simplisafe.mobile`." } diff --git a/homeassistant/components/simplisafe/translations/cs.json b/homeassistant/components/simplisafe/translations/cs.json index 520dcc2567e..b662da2f8c0 100644 --- a/homeassistant/components/simplisafe/translations/cs.json +++ b/homeassistant/components/simplisafe/translations/cs.json @@ -5,23 +5,9 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "identifier_exists": "\u00da\u010det je ji\u017e zaregistrov\u00e1n", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "reauth_confirm": { - "data": { - "password": "Heslo" - }, - "description": "Platnost va\u0161eho p\u0159\u00edstupov\u00e9ho tokenu vypr\u0161ela nebo byla zru\u0161ena. Chcete-li sv\u016fj \u00fa\u010det znovu propojit, zadejte sv\u00e9 heslo.", - "title": "Znovu ov\u011b\u0159it integraci" - }, - "user": { - "data": { - "password": "Heslo", - "username": "E-mail" - } - } } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/da.json b/homeassistant/components/simplisafe/translations/da.json index 8133eee3ec9..b500098acfa 100644 --- a/homeassistant/components/simplisafe/translations/da.json +++ b/homeassistant/components/simplisafe/translations/da.json @@ -2,14 +2,6 @@ "config": { "abort": { "already_configured": "Denne SimpliSafe-konto er allerede i brug." - }, - "step": { - "user": { - "data": { - "password": "Adgangskode", - "username": "Emailadresse" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 9ada40c252d..6ec9e183040 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", - "email_2fa_timed_out": "Zeit\u00fcberschreitung beim Warten auf E-Mail-basierte Zwei-Faktor-Authentifizierung.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "wrong_account": "Die angegebenen Benutzeranmeldeinformationen stimmen nicht mit diesem SimpliSafe-Konto \u00fcberein." }, @@ -12,36 +11,18 @@ "invalid_auth_code_length": "SimpliSafe Autorisierungscodes sind 45 Zeichen lang", "unknown": "Unerwarteter Fehler" }, - "progress": { - "email_2fa": "\u00dcberpr\u00fcfe deine E-Mails auf einen Best\u00e4tigungslink von Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Passwort" - }, - "description": "Bitte gib das Passwort f\u00fcr {username} erneut ein.", - "title": "Integration erneut authentifizieren" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Gib den Code f\u00fcr die Zwei-Faktor-Authentifizierung ein, den du per SMS erhalten hast." - }, "user": { "data": { - "auth_code": "Autorisierungscode", - "password": "Passwort", - "username": "Benutzername" + "auth_code": "Autorisierungscode" }, - "description": "SimpliSafe authentifiziert die Benutzer \u00fcber seine Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) liest, bevor du beginnst.\n\nWenn du bereit bist, dr\u00fccke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn du dich bereits bei SimpliSafe in deinem Browser angemeldet hast, kannst du eine neue Registerkarte \u00f6ffnen und dann die oben genannte URL in diese Registerkarte kopieren/einf\u00fcgen.\n\nWenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib den Autorisierungscode von der URL \"com.simplisafe.mobile\" ein." + "description": "SimpliSafe authentifiziert die Benutzer \u00fcber seine Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) liest, bevor du beginnst.\n\nWenn du bereit bist, dr\u00fccke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn du dich bereits bei SimpliSafe in deinem Browser angemeldet hast, kannst du eine neue Registerkarte \u00f6ffnen und dann die oben genannte URL in diese Registerkarte kopieren/einf\u00fcgen.\n\nSobald der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib den Autorisierungscode von der URL \"com.simplisafe.mobile\" ein." } } }, "issues": { "deprecated_service": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Ziel-Entit\u00e4ts-ID von `{alternate_target}` zu verwenden. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Ziel-Entit\u00e4ts ID von `{alternate_target}` zu verwenden. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", "title": "Der Dienst {deprecated_service} wird entfernt" } }, diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index d4cd15d93f0..896c5bb8909 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 SimpliSafe \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", - "email_2fa_timed_out": "\u03a4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ad\u03bb\u03b7\u03be\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 email.", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "wrong_account": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc SimpliSafe." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "\u039f\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03af \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 SimpliSafe \u03ad\u03c7\u03bf\u03c5\u03bd \u03bc\u03ae\u03ba\u03bf\u03c2 45 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "progress": { - "email_2fa": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd\n\u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b5\u03c3\u03c4\u03ac\u03bb\u03b7 \u03bc\u03ad\u03c3\u03c9 email." - }, "step": { - "reauth_confirm": { - "data": { - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u0397 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03bb\u03ae\u03be\u03b5\u03b9 \u03ae \u03b1\u03bd\u03b1\u03ba\u03bb\u03b7\u03b8\u03b5\u03af. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2.", - "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" - }, - "sms_2fa": { - "data": { - "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2" - }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b5\u03c3\u03c4\u03ac\u03bb\u03b7 \u03bc\u03ad\u03c3\u03c9 SMS." - }, "user": { "data": { - "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "username": "Email" + "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2" }, "description": "\u03a4\u03bf SimpliSafe \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf Home Assistant \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SimpliSafe web. \u039b\u03cc\u03b3\u03c9 \u03c4\u03b5\u03c7\u03bd\u03b9\u03ba\u03ce\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ce\u03bd, \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b2\u03ae\u03bc\u03b1 \u03c3\u03c4\u03bf \u03c4\u03ad\u03bb\u03bf\u03c2 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2- \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b1\u03b2\u03ac\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03c0\u03c1\u03b9\u03bd \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5.\n\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf [\u03b5\u03b4\u03ce]({url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae SimpliSafe web \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2.\n\n2. \u038c\u03c4\u03b1\u03bd \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2, \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03b5\u03b4\u03ce \u03ba\u03b1\u03b9 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2." } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 60e641a597c..724e3b5af28 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "auth_code": "Authorization Code", - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } diff --git a/homeassistant/components/simplisafe/translations/es-419.json b/homeassistant/components/simplisafe/translations/es-419.json index 868d4d4f53d..8001cd18849 100644 --- a/homeassistant/components/simplisafe/translations/es-419.json +++ b/homeassistant/components/simplisafe/translations/es-419.json @@ -2,14 +2,6 @@ "config": { "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso." - }, - "step": { - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Direcci\u00f3n de correo electr\u00f3nico" - } - } } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 38936c22ec2..3da88d4b576 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso.", - "email_2fa_timed_out": "Se agot\u00f3 el tiempo de espera para la autenticaci\u00f3n de dos factores basada en correo electr\u00f3nico.", "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "wrong_account": "Las credenciales de usuario proporcionadas no coinciden con esta cuenta de SimpliSafe." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "Los c\u00f3digos de autorizaci\u00f3n de SimpliSafe tienen 45 caracteres de longitud", "unknown": "Error inesperado" }, - "progress": { - "email_2fa": "Revisa tu correo electr\u00f3nico para obtener un enlace de verificaci\u00f3n de Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Contrase\u00f1a" - }, - "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}", - "title": "Volver a autenticar la integraci\u00f3n" - }, - "sms_2fa": { - "data": { - "code": "C\u00f3digo" - }, - "description": "Introduce el c\u00f3digo de autenticaci\u00f3n de dos factores que se te envi\u00f3 por SMS." - }, "user": { "data": { - "auth_code": "C\u00f3digo de Autorizaci\u00f3n", - "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "auth_code": "C\u00f3digo de Autorizaci\u00f3n" }, "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; por favor, aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index d1403a88303..1c1f65cd51d 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "See SimpliSafe'i konto on juba kasutusel.", - "email_2fa_timed_out": "Meilip\u00f5hise kahefaktorilise autentimise ajal\u00f5pp.", "reauth_successful": "Taastuvastamine \u00f5nnestus", "wrong_account": "Esitatud kasutaja mandaadid ei \u00fchti selle SimpliSafe kontoga." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "SimpliSafe autoriseerimiskoodid on 45 t\u00e4hem\u00e4rki pikad.", "unknown": "Tundmatu viga" }, - "progress": { - "email_2fa": "Kontrolli oma meili Simplisafe'i kinnituslingi saamiseks." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Salas\u00f5na" - }, - "description": "Taassisesta salas\u00f5na kasutajanimele {username}.", - "title": "Taastuvasta SimpliSafe'i konto" - }, - "sms_2fa": { - "data": { - "code": "Kood" - }, - "description": "Sisesta SMS-iga saadetud kahefaktoriline autentimiskood." - }, "user": { "data": { - "auth_code": "Tuvastuskood", - "password": "Salas\u00f5na", - "username": "Kasutajanimi" + "auth_code": "Tuvastuskood" }, "description": "SimpliSafe autentib kasutajad veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus manuaalne samm; palun lugege enne alustamist kindlasti [dokumentatsiooni](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui olete valmis, kl\u00f5psake [siin]({url}), et avada SimpliSafe veebirakendus ja sisestada oma volitused. Kui olete juba SimpliSafe'ile oma brauseris sisse loginud, siis avage uus vahekaart ja kopeerige/liidke \u00fclaltoodud URL sellesse vahekaarti.\n\nKui protsess on l\u00f5pule viidud, naaske siia ja sisestage autoriseerimiskood `com.simplisafe.mobile` URL-i." } diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index ed355f5faf8..0f9eb3f2023 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.", - "email_2fa_timed_out": "D\u00e9lai d'attente de l'authentification \u00e0 deux facteurs par courriel expir\u00e9.", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "wrong_account": "Les informations d'identification d'utilisateur fournies ne correspondent pas \u00e0 ce compte SimpliSafe." }, @@ -11,28 +10,10 @@ "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, - "progress": { - "email_2fa": "Vous devriez recevoir un lien de v\u00e9rification par courriel envoy\u00e9 par Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Mot de passe" - }, - "description": "Veuillez de nouveau saisir le mot de passe pour {username}.", - "title": "R\u00e9-authentifier l'int\u00e9gration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Saisissez le code d'authentification \u00e0 deux facteurs qui vous a \u00e9t\u00e9 envoy\u00e9 par SMS." - }, "user": { "data": { - "auth_code": "Code d'autorisation", - "password": "Mot de passe", - "username": "Nom d'utilisateur" + "auth_code": "Code d'autorisation" } } } diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json index 70ab1cff6ac..6cf7126ef70 100644 --- a/homeassistant/components/simplisafe/translations/he.json +++ b/homeassistant/components/simplisafe/translations/he.json @@ -6,26 +6,6 @@ "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "reauth_confirm": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username}.", - "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" - }, - "sms_2fa": { - "data": { - "code": "\u05e7\u05d5\u05d3" - } - }, - "user": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index d908785a266..1d2f4842c76 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", - "email_2fa_timed_out": "Az e-mail alap\u00fa k\u00e9tfaktoros hiteles\u00edt\u00e9sre val\u00f3 v\u00e1rakoz\u00e1s ideje lej\u00e1rt", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "A SimpliSafe enged\u00e9lyez\u00e9si k\u00f3dok 45 karakter hossz\u00faak.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "progress": { - "email_2fa": "Ellen\u0151rizze az e-mailj\u00e9t a Simplisafe \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 link\u00e9rt." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Jelsz\u00f3" - }, - "description": "K\u00e9rem, adja meg ism\u00e9t {username} jelszav\u00e1t.", - "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" - }, - "sms_2fa": { - "data": { - "code": "K\u00f3d" - }, - "description": "\u00cdrja be a k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3dot, amelyet SMS-ben k\u00fcldtek \u00d6nnek." - }, "user": { "data": { - "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d", - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d" }, "description": "A SimpliSafe a webes alkalmaz\u00e1son kereszt\u00fcl hiteles\u00edti a felhaszn\u00e1l\u00f3kat. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy manu\u00e1lis l\u00e9p\u00e9s; k\u00e9rj\u00fck, hogy a kezd\u00e9s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webes alkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s adja meg a hiteles\u00edt\u0151 adatokat. Ha m\u00e1r bejelentkezett a SimpliSafe rendszerbe a b\u00f6ng\u00e9sz\u0151ben, akkor \u00e9rdemes egy \u00faj lapot nyitni, majd a fenti URL-t bem\u00e1solni/beilleszteni abba a lapba.\n\nHa a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg az enged\u00e9lyez\u00e9si k\u00f3dot a `com.simplisafe.mobile` URL-r\u0151l." } diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 0bb888eceed..66614fffdb4 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Akun SimpliSafe ini sudah digunakan.", - "email_2fa_timed_out": "Tenggang waktu habis ketika menunggu autentikasi dua faktor berbasis email.", "reauth_successful": "Autentikasi ulang berhasil", "wrong_account": "Kredensial pengguna yang diberikan tidak cocok dengan akun SimpliSafe ini." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "Panjang kode otorisasi SimpliSafe adalah 45 karakter", "unknown": "Kesalahan yang tidak diharapkan" }, - "progress": { - "email_2fa": "Periksa email Anda untuk tautan verifikasi dari Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Kata Sandi" - }, - "description": "Masukkan kembali kata sandi untuk {username}.", - "title": "Autentikasi Ulang Integrasi" - }, - "sms_2fa": { - "data": { - "code": "Kode" - }, - "description": "Masukkan kode autentikasi dua faktor yang dikirimkan kepada Anda melalui SMS." - }, "user": { "data": { - "auth_code": "Kode Otorisasi", - "password": "Kata Sandi", - "username": "Nama Pengguna" + "auth_code": "Kode Otorisasi" }, "description": "SimpliSafe mengautentikasi pengguna melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan bahwa Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika sudah siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan memasukkan kredensial Anda. Jika Anda sedang masuk ke SimpliSafe di browser, Anda mungkin harus membuka tab baru, kemudian menyalin-tempel URL di atas di tab baru tersebut.\n\nSetelah proses selesai, kembali ke sini dan masukkan kode otorisasi dari URL `com.simplisafe.mobile`." } diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index dda07835a81..b261ce66ade 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", - "email_2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori basata su email.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "wrong_account": "Le credenziali utente fornite non corrispondono a questo account SimpliSafe." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "I codici di autorizzazione SimpliSafe sono lunghi 45 caratteri", "unknown": "Errore imprevisto" }, - "progress": { - "email_2fa": "Verifica la presenza nella tua email di un collegamento di verifica da Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Digita nuovamente la password per {username}.", - "title": "Autentica nuovamente l'integrazione" - }, - "sms_2fa": { - "data": { - "code": "Codice" - }, - "description": "Digita il codice di autenticazione a due fattori che ti \u00e8 stato inviato tramite SMS." - }, "user": { "data": { - "auth_code": "Codice di autorizzazione", - "password": "Password", - "username": "Nome utente" + "auth_code": "Codice di autorizzazione" }, "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app web di SimpliSafe e inserisci le tue credenziali. Se hai gi\u00e0 effettuato l'accesso a SimpliSafe nel tuo browser, puoi aprire una nuova scheda e copiare/incollare l'URL sopra indicato in quella nuova scheda.\n\nAl termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL `com.simplisafe.mobile`." } diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index c448222f286..55a0367b54d 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u3053\u306eSimpliSafe account\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "email_2fa_timed_out": "\u96fb\u5b50\u30e1\u30fc\u30eb\u306b\u3088\u308b2\u8981\u7d20\u8a8d\u8a3c\u306e\u5f85\u6a5f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u306f\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "SimpliSafe\u306e\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306f45\u6587\u5b57\u3067\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, - "progress": { - "email_2fa": "Simplisafe\u304b\u3089\u306e\u78ba\u8a8d\u30ea\u30f3\u30af\u304c\u306a\u3044\u304b\u30e1\u30fc\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - }, "step": { - "reauth_confirm": { - "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" - }, - "sms_2fa": { - "data": { - "code": "\u30b3\u30fc\u30c9" - }, - "description": "SMS\u3067\u9001\u3089\u308c\u3066\u304d\u305f2\u8981\u7d20\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002" - }, "user": { "data": { - "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "E\u30e1\u30fc\u30eb" + "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" }, "description": "SimpliSafe \u306f\u3001Web \u30a2\u30d7\u30ea\u3092\u4ecb\u3057\u3066\u30e6\u30fc\u30b6\u30fc\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306b\u3088\u308a\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002 \n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]( {url} ) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066 SimpliSafe Web \u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u30d6\u30e9\u30a6\u30b6\u3067\u65e2\u306b SimpliSafe \u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3044\u308b\u5834\u5408\u306f\u3001\u65b0\u3057\u3044\u30bf\u30d6\u3092\u958b\u304d\u3001\u4e0a\u8a18\u306e URL \u3092\u30b3\u30d4\u30fc\u3057\u3066\u305d\u306e\u30bf\u30d6\u306b\u8cbc\u308a\u4ed8\u3051\u307e\u3059\u3002 \n\n\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u308a\u3001\u300ccom.simplisafe.mobile\u300d\u306e URL \u304b\u3089\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002" } diff --git a/homeassistant/components/simplisafe/translations/ko.json b/homeassistant/components/simplisafe/translations/ko.json index 4cbc24533e0..2c04b58d413 100644 --- a/homeassistant/components/simplisafe/translations/ko.json +++ b/homeassistant/components/simplisafe/translations/ko.json @@ -7,24 +7,6 @@ "error": { "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "progress": { - "email_2fa": "\uc774\uba54\uc77c\uc5d0\uc11c Simplisafe\uc758 \uc778\uc99d \ub9c1\ud06c\ub97c \ud655\uc778\ud558\uc2ed\uc2dc\uc624." - }, - "step": { - "reauth_confirm": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ub9cc\ub8cc\ub418\uc5c8\uac70\ub098 \ud574\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uacc4\uc815\uc744 \ub2e4\uc2dc \uc5f0\uacb0\ud558\ub824\uba74 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" - }, - "user": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc774\uba54\uc77c" - } - } } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/lb.json b/homeassistant/components/simplisafe/translations/lb.json index e9034565e93..33ec46a86f3 100644 --- a/homeassistant/components/simplisafe/translations/lb.json +++ b/homeassistant/components/simplisafe/translations/lb.json @@ -7,21 +7,6 @@ "error": { "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" - }, - "step": { - "reauth_confirm": { - "data": { - "password": "Passwuert" - }, - "description": "D\u00e4in Acc\u00e8s Jeton as ofgelaf oder gouf revok\u00e9iert. G\u00ebff d\u00e4i Passwuert an fir d\u00e4i Kont fr\u00ebsch ze verbannen.", - "title": "Integratioun re-authentifiz\u00e9ieren" - }, - "user": { - "data": { - "password": "Passwuert", - "username": "E-Mail" - } - } } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index b24bde70fac..d7da5fbd4ce 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -2,35 +2,14 @@ "config": { "abort": { "already_configured": "Dit SimpliSafe-account is al in gebruik.", - "email_2fa_timed_out": "Timed out tijdens het wachten op email-gebaseerde twee-factor authenticatie.", "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "progress": { - "email_2fa": "Controleer uw e-mail voor een verificatielink van Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Wachtwoord" - }, - "description": "Voer het wachtwoord voor {username} opnieuw in.", - "title": "Integratie herauthenticeren" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Voer de twee-factor authenticatiecode in die u heeft ontvangen via SMS" - }, "user": { - "data": { - "password": "Wachtwoord", - "username": "Gebruikersnaam" - }, "description": "Voer uw gebruikersnaam en wachtwoord in." } } diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 5d6f81c4444..3ffa0f65387 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", - "email_2fa_timed_out": "Tidsavbrudd mens du ventet p\u00e5 e-postbasert tofaktorautentisering.", "reauth_successful": "Re-autentisering var vellykket", "wrong_account": "Oppgitt brukerlegitimasjon samsvarer ikke med denne SimpliSafe-kontoen." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "SimpliSafe-autorisasjonskoder er p\u00e5 45 tegn", "unknown": "Uventet feil" }, - "progress": { - "email_2fa": "Sjekk e-posten din for en bekreftelseslenke fra Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Passord" - }, - "description": "Vennligst skriv inn passordet for {username} p\u00e5 nytt.", - "title": "Godkjenne integrering p\u00e5 nytt" - }, - "sms_2fa": { - "data": { - "code": "Kode" - }, - "description": "Skriv inn tofaktorautentiseringskoden sendt til deg via SMS." - }, "user": { "data": { - "auth_code": "Autorisasjonskode", - "password": "Passord", - "username": "Brukernavn" + "auth_code": "Autorisasjonskode" }, "description": "SimpliSafe autentiserer brukere via sin nettapp. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. Hvis du allerede har logget p\u00e5 SimpliSafe i nettleseren din, kan det v\u00e6re lurt \u00e5 \u00e5pne en ny fane, og deretter kopiere/lime inn URL-en ovenfor i den fanen. \n\n N\u00e5r prosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden fra `com.simplisafe.mobile` URL." } diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 0af42cadfd3..cbdba2d95f2 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu", - "email_2fa_timed_out": "Przekroczono limit czasu oczekiwania na e-mailowe uwierzytelnianie dwusk\u0142adnikowe.", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "wrong_account": "Podane dane uwierzytelniaj\u0105ce u\u017cytkownika nie pasuj\u0105 do tego konta SimpliSafe." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "Kody autoryzacji SimpliSafe maj\u0105 d\u0142ugo\u015b\u0107 45 znak\u00f3w", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "progress": { - "email_2fa": "Sprawd\u017a e-mail z linkiem weryfikacyjnym od Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Has\u0142o" - }, - "description": "Wprowad\u017a ponownie has\u0142o dla u\u017cytkownika {username}.", - "title": "Ponownie uwierzytelnij integracj\u0119" - }, - "sms_2fa": { - "data": { - "code": "Kod" - }, - "description": "Wprowad\u017a kod uwierzytelniania dwusk\u0142adnikowego, kt\u00f3ry zosta\u0142 wys\u0142any do Ciebie SMS-em." - }, "user": { "data": { - "auth_code": "Kod autoryzacji", - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "auth_code": "Kod autoryzacji" }, "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Je\u015bli ju\u017c zalogowa\u0142e\u015b si\u0119 do Simplisafe w swojej przegl\u0105darce, mo\u017cesz otworzy\u0107 now\u0105 kart\u0119, a nast\u0119pnie skopiowa\u0107/wklei\u0107 powy\u017cszy adres URL.\n\nPo zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL 'com.simplisafe.mobile'." } diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 4dadf67720f..c5c0c399df4 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "A conta j\u00e1 foi configurada", - "email_2fa_timed_out": "Expirou enquanto aguardava a autentica\u00e7\u00e3o de dois fatores enviada por e-mail.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "wrong_account": "As credenciais de usu\u00e1rio fornecidas n\u00e3o correspondem a esta conta SimpliSafe." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "Os c\u00f3digos de autoriza\u00e7\u00e3o SimpliSafe t\u00eam 45 caracteres", "unknown": "Erro inesperado" }, - "progress": { - "email_2fa": "Verifique seu e-mail para obter um link de verifica\u00e7\u00e3o do Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Senha" - }, - "description": "Por favor, digite novamente a senha para {username}.", - "title": "Reautenticar Integra\u00e7\u00e3o" - }, - "sms_2fa": { - "data": { - "code": "C\u00f3digo" - }, - "description": "Insira o c\u00f3digo de autentica\u00e7\u00e3o de dois fatores enviado a voc\u00ea via SMS." - }, "user": { "data": { - "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o", - "password": "Senha", - "username": "Usu\u00e1rio" + "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o" }, "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo da web SimpliSafe e insira suas credenciais. Se voc\u00ea j\u00e1 fez login no SimpliSafe em seu navegador, voc\u00ea pode querer abrir uma nova guia e copiar/colar o URL acima nessa guia. \n\n Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL `com.simplisafe.mobile`." } diff --git a/homeassistant/components/simplisafe/translations/pt.json b/homeassistant/components/simplisafe/translations/pt.json index ba84255bc9d..f7ccc23fedd 100644 --- a/homeassistant/components/simplisafe/translations/pt.json +++ b/homeassistant/components/simplisafe/translations/pt.json @@ -6,20 +6,6 @@ "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" - }, - "step": { - "reauth_confirm": { - "data": { - "password": "Palavra-passe" - }, - "title": "Reautenticar integra\u00e7\u00e3o" - }, - "user": { - "data": { - "password": "Palavra-passe", - "username": "Email" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 327cb296ff3..afebb27e5aa 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -2,7 +2,6 @@ "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.", - "email_2fa_timed_out": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "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.", "wrong_account": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SimpliSafe." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "\u041a\u043e\u0434\u044b \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 SimpliSafe \u0441\u043e\u0441\u0442\u043e\u044f\u0442 \u0438\u0437 45 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "progress": { - "email_2fa": "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0432\u043e\u044e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443\u044e \u043f\u043e\u0447\u0442\u0443 \u043d\u0430 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u0441\u0441\u044b\u043b\u043a\u0438 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043e\u0442 Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username}.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" - }, - "sms_2fa": { - "data": { - "code": "\u041a\u043e\u0434" - }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c \u0432 SMS." - }, "user": { "data": { - "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438" }, "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\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](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u043e\u0448\u043b\u0438 \u0432 SimpliSafe \u0432 \u0441\u0432\u043e\u0435\u043c \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0432\u043a\u043b\u0430\u0434\u043a\u0443, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0432\u044b\u0448\u0435\u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.\n\n\u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 `com.simplisafe.mobile`." } diff --git a/homeassistant/components/simplisafe/translations/sk.json b/homeassistant/components/simplisafe/translations/sk.json index f59069125d0..93b939b88e1 100644 --- a/homeassistant/components/simplisafe/translations/sk.json +++ b/homeassistant/components/simplisafe/translations/sk.json @@ -1,18 +1,26 @@ { "config": { "abort": { + "already_configured": "Toto konto SimpliSafe sa u\u017e pou\u017e\u00edva.", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "identifier_exists": "\u00da\u010det je u\u017e zaregistrovan\u00fd", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "password": "Heslo", - "username": "Email" + "auth_code": "Autoriza\u010dn\u00fd k\u00f3d" } } } + }, + "issues": { + "deprecated_service": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`. Potom kliknite na tla\u010didlo SUBMIT (Odosla\u0165) ni\u017e\u0161ie a ozna\u010dte tento probl\u00e9m ako vyrie\u0161en\u00fd.", + "title": "Slu\u017eba {deprecated_service} sa odstra\u0148uje" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/sl.json b/homeassistant/components/simplisafe/translations/sl.json index d98ad9c20c7..d78f80e074a 100644 --- a/homeassistant/components/simplisafe/translations/sl.json +++ b/homeassistant/components/simplisafe/translations/sl.json @@ -2,14 +2,6 @@ "config": { "abort": { "already_configured": "Ta ra\u010dun SimpliSafe je \u017ee v uporabi." - }, - "step": { - "user": { - "data": { - "password": "Geslo", - "username": "E-po\u0161tni naslov" - } - } } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index a2d75f36697..87a013a68e9 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats.", - "email_2fa_timed_out": "Tidsgr\u00e4nsen tog slut i v\u00e4ntan p\u00e5 tv\u00e5faktorsautentisering", "reauth_successful": "\u00c5terautentisering lyckades", "wrong_account": "De angivna anv\u00e4ndaruppgifterna matchar inte detta SimpliSafe-konto." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "SimpliSafe auktoriseringskoder \u00e4r 45 tecken l\u00e5nga", "unknown": "Ov\u00e4ntat fel" }, - "progress": { - "email_2fa": "Kontrollera din e-post f\u00f6r en verifieringsl\u00e4nk fr\u00e5n Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "L\u00f6senord" - }, - "description": "Skriv om l\u00f6senordet f\u00f6r {username}", - "title": "\u00c5terautenticera integration" - }, - "sms_2fa": { - "data": { - "code": "Kod" - }, - "description": "Ange tv\u00e5faktorsautentiseringskoden som skickats till dig via SMS." - }, "user": { "data": { - "auth_code": "Auktoriseringskod", - "password": "L\u00f6senord", - "username": "E-postadress" + "auth_code": "Auktoriseringskod" }, "description": "SimpliSafe autentiserar anv\u00e4ndare via sin webbapp. P\u00e5 grund av tekniska begr\u00e4nsningar finns det ett manuellt steg i slutet av denna process; se till att du l\u00e4ser [dokumentationen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) innan du b\u00f6rjar. \n\n N\u00e4r du \u00e4r redo klickar du [h\u00e4r]( {url} ) f\u00f6r att \u00f6ppna webbappen SimpliSafe och ange dina referenser. N\u00e4r processen \u00e4r klar, \u00e5terv\u00e4nd hit och mata in auktoriseringskoden fr\u00e5n SimpliSafe-webbappens URL." } diff --git a/homeassistant/components/simplisafe/translations/th.json b/homeassistant/components/simplisafe/translations/th.json deleted file mode 100644 index 84fcb89add1..00000000000 --- a/homeassistant/components/simplisafe/translations/th.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "\u0e17\u0e35\u0e48\u0e2d\u0e22\u0e39\u0e48\u0e2d\u0e35\u0e40\u0e21\u0e25" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index f7073bb3df7..70f327e9967 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", - "email_2fa_timed_out": "E-posta tabanl\u0131 iki fakt\u00f6rl\u00fc kimlik do\u011frulama i\u00e7in beklerken zaman a\u015f\u0131m\u0131na u\u011frad\u0131.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "wrong_account": "Sa\u011flanan kullan\u0131c\u0131 kimlik bilgileri bu SimpliSafe hesab\u0131yla e\u015fle\u015fmiyor." }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "SimpliSafe yetkilendirme kodlar\u0131 45 karakter uzunlu\u011fundad\u0131r", "unknown": "Beklenmeyen hata" }, - "progress": { - "email_2fa": "Simplisafe'den bir do\u011frulama ba\u011flant\u0131s\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Parola" - }, - "description": "L\u00fctfen {username} \u015fifresini tekrar girin.", - "title": "Entegrasyonu Yeniden Do\u011frula" - }, - "sms_2fa": { - "data": { - "code": "Kod" - }, - "description": "Size SMS ile g\u00f6nderilen iki fakt\u00f6rl\u00fc do\u011frulama kodunu girin." - }, "user": { "data": { - "auth_code": "Yetkilendirme Kodu", - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "auth_code": "Yetkilendirme Kodu" }, "description": "SimpliSafe, web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla kullan\u0131c\u0131lar\u0131n kimli\u011fini do\u011frular. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) okudu\u011funuzdan emin olun. \n\n Haz\u0131r oldu\u011funuzda SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]( {url} ) t\u0131klay\u0131n. Taray\u0131c\u0131n\u0131zda SimpliSafe'e zaten giri\u015f yapt\u0131ysan\u0131z, yeni bir sekme a\u00e7mak ve ard\u0131ndan yukar\u0131daki URL'yi o sekmeye kopyalay\u0131p/yap\u0131\u015ft\u0131rmak isteyebilirsiniz. \n\n \u0130\u015flem tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve \"com.simplisafe.mobile\" URL'sinden yetkilendirme kodunu girin." } diff --git a/homeassistant/components/simplisafe/translations/uk.json b/homeassistant/components/simplisafe/translations/uk.json index 1df0e00c6cc..6d86c8572b4 100644 --- a/homeassistant/components/simplisafe/translations/uk.json +++ b/homeassistant/components/simplisafe/translations/uk.json @@ -7,21 +7,6 @@ "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": { - "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": { - "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" - } - } } }, "issues": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hans.json b/homeassistant/components/simplisafe/translations/zh-Hans.json index d28951425ec..2750a3ae308 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hans.json +++ b/homeassistant/components/simplisafe/translations/zh-Hans.json @@ -2,14 +2,6 @@ "config": { "error": { "invalid_auth": "\u65e0\u6548\u7684\u8eab\u4efd\u9a8c\u8bc1" - }, - "step": { - "user": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 199e96e6792..a7e31e74341 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002", - "email_2fa_timed_out": "\u7b49\u5f85\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u90f5\u4ef6\u903e\u6642\u3002", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "wrong_account": "\u6240\u4ee5\u63d0\u4f9b\u7684\u6191\u8b49\u8207 Simplisafe \u5e33\u865f\u4e0d\u7b26\u3002" }, @@ -12,28 +11,10 @@ "invalid_auth_code_length": "SimpliSafe \u8a8d\u8b49\u78bc\u70ba 45 \u500b\u5b57\u5143\u9577", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "progress": { - "email_2fa": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 \u7684Simplisafe \u9a57\u8b49\u9023\u7d50\u3002" - }, "step": { - "reauth_confirm": { - "data": { - "password": "\u5bc6\u78bc" - }, - "description": "\u8acb\u8f38\u5165\u5e33\u865f {username} \u5bc6\u78bc\u3002", - "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" - }, - "sms_2fa": { - "data": { - "code": "\u9a57\u8b49\u78bc" - }, - "description": "\u8f38\u5165\u7c21\u8a0a\u6240\u6536\u5230\u7684\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u3002" - }, "user": { "data": { - "auth_code": "\u8a8d\u8b49\u78bc", - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" + "auth_code": "\u8a8d\u8b49\u78bc" }, "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5047\u5982\u5df2\u7d93\u65bc\u700f\u89bd\u5668\u4e2d\u767b\u5165 SimpliSafe\uff0c\u53ef\u80fd\u9700\u8981\u958b\u555f\u65b0\u9801\u9762\u3001\u7136\u5f8c\u65bc\u9801\u9762\u7db2\u5740\u8907\u88fd/\u8cbc\u4e0a\u4e0a\u65b9 URL\u3002\n\n\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 `com.simplisafe.mobile` \u6240\u53d6\u5f97\u7684\u8a8d\u8b49\u78bc\u3002" } diff --git a/homeassistant/components/simu/__init__.py b/homeassistant/components/simu/__init__.py new file mode 100644 index 00000000000..7b02a1109f7 --- /dev/null +++ b/homeassistant/components/simu/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: SIMU LiveIn2.""" diff --git a/homeassistant/components/simu/manifest.json b/homeassistant/components/simu/manifest.json new file mode 100644 index 00000000000..a1cf964f438 --- /dev/null +++ b/homeassistant/components/simu/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "simu", + "name": "SIMU LiveIn2", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 920865c41f3..92d547d5760 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -64,9 +64,8 @@ def process_turn_on_params( Filters out unsupported params and validates the rest. """ - supported_features = siren.supported_features or 0 - if not supported_features & SirenEntityFeature.TONES: + if not siren.supported_features & SirenEntityFeature.TONES: params.pop(ATTR_TONE, None) elif (tone := params.get(ATTR_TONE)) is not None: # Raise an exception if the specified tone isn't available @@ -92,9 +91,9 @@ def process_turn_on_params( key for key, value in siren.available_tones.items() if value == tone ) - if not supported_features & SirenEntityFeature.DURATION: + if not siren.supported_features & SirenEntityFeature.DURATION: params.pop(ATTR_DURATION, None) - if not supported_features & SirenEntityFeature.VOLUME_SET: + if not siren.supported_features & SirenEntityFeature.VOLUME_SET: params.pop(ATTR_VOLUME_LEVEL, None) return params @@ -163,15 +162,14 @@ class SirenEntity(ToggleEntity): entity_description: SirenEntityDescription _attr_available_tones: list[int | str] | dict[int, str] | None + _attr_supported_features: SirenEntityFeature = SirenEntityFeature(0) @final @property def capability_attributes(self) -> dict[str, Any] | None: """Return capability attributes.""" - supported_features = self.supported_features or 0 - if ( - supported_features & SirenEntityFeature.TONES + self.supported_features & SirenEntityFeature.TONES and self.available_tones is not None ): return {ATTR_AVAILABLE_TONES: self.available_tones} @@ -190,3 +188,8 @@ class SirenEntity(ToggleEntity): if hasattr(self, "entity_description"): return self.entity_description.available_tones return None + + @property + def supported_features(self) -> SirenEntityFeature: + """Return the list of supported features.""" + return self._attr_supported_features diff --git a/homeassistant/components/siren/const.py b/homeassistant/components/siren/const.py index 6ef7c9e6955..374b1d59e2a 100644 --- a/homeassistant/components/siren/const.py +++ b/homeassistant/components/siren/const.py @@ -1,6 +1,6 @@ """Constants for the siren component.""" -from enum import IntEnum +from enum import IntFlag from typing import Final DOMAIN: Final = "siren" @@ -12,7 +12,7 @@ ATTR_DURATION: Final = "duration" ATTR_VOLUME_LEVEL: Final = "volume_level" -class SirenEntityFeature(IntEnum): +class SirenEntityFeature(IntFlag): """Supported features of the siren entity.""" TURN_ON = 1 diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index bfef4bc3422..059ba76febc 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["ffmpeg"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "loggers": ["aioskybell"] + "loggers": ["aioskybell"], + "integration_type": "hub" } diff --git a/homeassistant/components/skybell/translations/bg.json b/homeassistant/components/skybell/translations/bg.json index 556fba19234..b6c96b5290e 100644 --- a/homeassistant/components/skybell/translations/bg.json +++ b/homeassistant/components/skybell/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/skybell/translations/he.json b/homeassistant/components/skybell/translations/he.json index 0e3ced77bc3..f85a3c23279 100644 --- a/homeassistant/components/skybell/translations/he.json +++ b/homeassistant/components/skybell/translations/he.json @@ -13,7 +13,8 @@ "reauth_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - } + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, "user": { "data": { diff --git a/homeassistant/components/skybell/translations/sk.json b/homeassistant/components/skybell/translations/sk.json new file mode 100644 index 00000000000..e3d6831b30d --- /dev/null +++ b/homeassistant/components/skybell/translations/sk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Aktualizujte svoje heslo pre {email}", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "email": "Email", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index a89f645e9b6..36b457b75b7 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -1,4 +1,6 @@ """The slack integration.""" +from __future__ import annotations + import logging from aiohttp.client_exceptions import ClientError @@ -10,13 +12,23 @@ from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, discovery +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.typing import ConfigType -from .const import DATA_CLIENT, DATA_HASS_CONFIG, DOMAIN +from .const import ( + ATTR_URL, + ATTR_USER_ID, + DATA_CLIENT, + DATA_HASS_CONFIG, + DEFAULT_NAME, + DOMAIN, + SLACK_DATA, +) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.NOTIFY] +PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -42,14 +54,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: slack = WebClient(token=entry.data[CONF_API_KEY], run_async=True, session=session) try: - await slack.auth_test() + res = await slack.auth_test() except (SlackApiError, ClientError) as ex: if isinstance(ex, SlackApiError) and ex.response["error"] == "invalid_auth": _LOGGER.error("Invalid API key") return False raise ConfigEntryNotReady("Error while setting up integration") from ex - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data | {DATA_CLIENT: slack} + data = { + DATA_CLIENT: slack, + ATTR_URL: res[ATTR_URL], + ATTR_USER_ID: res[ATTR_USER_ID], + } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data | {SLACK_DATA: data} hass.async_create_task( discovery.async_load_platform( @@ -61,4 +77,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + await hass.config_entries.async_forward_entry_setups( + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] + ) + return True + + +class SlackEntity(Entity): + """Representation of a Slack entity.""" + + _attr_attribution = "Data provided by Slack" + _attr_has_entity_name = True + + def __init__( + self, + data: dict[str, str | WebClient], + description: EntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize a Slack entity.""" + self._client = data[DATA_CLIENT] + self.entity_description = description + self._attr_unique_id = f"{data[ATTR_USER_ID]}_{description.key}" + self._attr_device_info = DeviceInfo( + configuration_url=data[ATTR_URL], + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer=DEFAULT_NAME, + name=entry.title, + ) diff --git a/homeassistant/components/slack/const.py b/homeassistant/components/slack/const.py index 83937f4a43e..ec0993e290b 100644 --- a/homeassistant/components/slack/const.py +++ b/homeassistant/components/slack/const.py @@ -6,13 +6,17 @@ ATTR_BLOCKS_TEMPLATE = "blocks_template" ATTR_FILE = "file" ATTR_PASSWORD = "password" ATTR_PATH = "path" +ATTR_SNOOZE = "snooze_endtime" ATTR_URL = "url" ATTR_USERNAME = "username" +ATTR_USER_ID = "user_id" CONF_DEFAULT_CHANNEL = "default_channel" DATA_CLIENT = "client" +DEFAULT_NAME = "Slack" DEFAULT_TIMEOUT_SECONDS = 15 DOMAIN: Final = "slack" +SLACK_DATA = "data" DATA_HASS_CONFIG = "slack_hass_config" diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index bfcf79ef676..d587f960704 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -41,6 +41,7 @@ from .const import ( ATTR_USERNAME, CONF_DEFAULT_CHANNEL, DATA_CLIENT, + SLACK_DATA, ) _LOGGER = logging.getLogger(__name__) @@ -121,7 +122,7 @@ async def async_get_service( return SlackNotificationService( hass, - discovery_info.pop(DATA_CLIENT), + discovery_info[SLACK_DATA][DATA_CLIENT], discovery_info, ) diff --git a/homeassistant/components/slack/sensor.py b/homeassistant/components/slack/sensor.py new file mode 100644 index 00000000000..b190e6151ed --- /dev/null +++ b/homeassistant/components/slack/sensor.py @@ -0,0 +1,53 @@ +"""Slack platform for sensor component.""" +from __future__ import annotations + +from slack import WebClient + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util + +from . import SlackEntity +from .const import ATTR_SNOOZE, DOMAIN, SLACK_DATA + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Slack select.""" + async_add_entities( + [ + SlackSensorEntity( + hass.data[DOMAIN][entry.entry_id][SLACK_DATA], + SensorEntityDescription( + key="do_not_disturb_until", + name="Do not disturb until", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + ), + entry, + ) + ], + True, + ) + + +class SlackSensorEntity(SlackEntity, SensorEntity): + """Representation of a Slack sensor.""" + + _client: WebClient + + async def async_update(self) -> None: + """Get the latest status.""" + if _time := (await self._client.dnd_info()).get(ATTR_SNOOZE): + self._attr_native_value = dt_util.utc_from_timestamp(_time) + else: + self._attr_native_value = None diff --git a/homeassistant/components/slack/translations/sk.json b/homeassistant/components/slack/translations/sk.json index 4e2a5fa0ed7..7d542d6c565 100644 --- a/homeassistant/components/slack/translations/sk.json +++ b/homeassistant/components/slack/translations/sk.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", + "default_channel": "Predvolen\u00fd kan\u00e1l", "icon": "Ikona", "username": "U\u017e\u00edvate\u013esk\u00e9 meno" }, diff --git a/homeassistant/components/sleepiq/translations/bg.json b/homeassistant/components/sleepiq/translations/bg.json index e1494bd66a1..a0a6aa3235c 100644 --- a/homeassistant/components/sleepiq/translations/bg.json +++ b/homeassistant/components/sleepiq/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/sleepiq/translations/sk.json b/homeassistant/components/sleepiq/translations/sk.json index 5ada995aa6e..249ddf45926 100644 --- a/homeassistant/components/sleepiq/translations/sk.json +++ b/homeassistant/components/sleepiq/translations/sk.json @@ -1,7 +1,25 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/sk.json b/homeassistant/components/sma/translations/sk.json index 0b7bf878ea9..0196a2c43f3 100644 --- a/homeassistant/components/sma/translations/sk.json +++ b/homeassistant/components/sma/translations/sk.json @@ -1,10 +1,24 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "cannot_retrieve_device_info": "\u00daspe\u0161ne pripojen\u00e9, ale nie je mo\u017en\u00e9 z\u00edska\u0165 inform\u00e1cie o zariaden\u00ed", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "group": "Skupina", + "host": "Hostite\u013e", + "password": "Heslo", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index 7c18d944ab7..7b3a0472f41 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere il dispositivo Smappee con numero di serie `{serialnumber}` a Home Assistant?", - "title": "Dispositivo Smappee rilevato" + "title": "Rilevato dispositivo Smappee" } } } diff --git a/homeassistant/components/smappee/translations/sk.json b/homeassistant/components/smappee/translations/sk.json new file mode 100644 index 00000000000..d135a72ae63 --- /dev/null +++ b/homeassistant/components/smappee/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured_device": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_mdns": "Nepodporovan\u00e9 zariadenie pre integr\u00e1ciu Smappee.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})" + }, + "flow_title": "{name}", + "step": { + "local": { + "data": { + "host": "Hostite\u013e" + } + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/sk.json b/homeassistant/components/smart_meter_texas/translations/sk.json index 5ada995aa6e..0c9a112e32e 100644 --- a/homeassistant/components/smart_meter_texas/translations/sk.json +++ b/homeassistant/components/smart_meter_texas/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 0ff3a82d788..d2d0dba6773 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -70,8 +70,6 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: class SmartThingsCover(SmartThingsEntity, CoverEntity): """Define a SmartThings cover.""" - _attr_supported_features: int - def __init__(self, device): """Initialize the cover class.""" super().__init__(device) @@ -102,7 +100,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - if not self._attr_supported_features & CoverEntityFeature.SET_POSITION: + if not self.supported_features & CoverEntityFeature.SET_POSITION: return # Do not set_status=True as device will report progress. await self._device.set_level(kwargs[ATTR_POSITION], 0) @@ -144,7 +142,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): @property def current_cover_position(self) -> int | None: """Return current position of cover.""" - if not self._attr_supported_features & CoverEntityFeature.SET_POSITION: + if not self.supported_features & CoverEntityFeature.SET_POSITION: return None return self._device.status.level diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index ccf63582e86..37237323d1c 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -101,9 +101,9 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): return color_modes - def _determine_features(self): + def _determine_features(self) -> LightEntityFeature: """Get features supported by the device.""" - features = 0 + features = LightEntityFeature(0) # Transition if Capability.switch_level in self._device.capabilities: features |= LightEntityFeature.TRANSITION diff --git a/homeassistant/components/smartthings/translations/sk.json b/homeassistant/components/smartthings/translations/sk.json index 534c1e859ee..0f2b3e1f616 100644 --- a/homeassistant/components/smartthings/translations/sk.json +++ b/homeassistant/components/smartthings/translations/sk.json @@ -10,6 +10,9 @@ "data": { "location_id": "Umiestnenie" } + }, + "user": { + "title": "Potvr\u010fte URL sp\u00e4tn\u00e9ho volania" } } } diff --git a/homeassistant/components/smarttub/translations/sk.json b/homeassistant/components/smarttub/translations/sk.json index f8b6dfeea81..ebc9c699c89 100644 --- a/homeassistant/components/smarttub/translations/sk.json +++ b/homeassistant/components/smarttub/translations/sk.json @@ -1,16 +1,23 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { - "email": "Email" - } + "email": "Email", + "password": "Heslo" + }, + "description": "Na prihl\u00e1senie zadajte svoju e-mailov\u00fa adresu a heslo SmartTub", + "title": "Prihl\u00e1si\u0165" } } } diff --git a/homeassistant/components/smhi/translations/hr.json b/homeassistant/components/smhi/translations/hr.json new file mode 100644 index 00000000000..0ceab312f1e --- /dev/null +++ b/homeassistant/components/smhi/translations/hr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "wrong_location": "Lokacija samo \u0160vedska" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina" + }, + "title": "Lokacija u \u0160vedskoj" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/sk.json b/homeassistant/components/smhi/translations/sk.json index e6945904d90..60b95d94721 100644 --- a/homeassistant/components/smhi/translations/sk.json +++ b/homeassistant/components/smhi/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index a02a627b7f2..12781df6891 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -45,11 +45,11 @@ from homeassistant.const import ( CONF_LOCATION, CONF_LONGITUDE, CONF_NAME, - LENGTH_KILOMETERS, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client @@ -121,11 +121,11 @@ class SmhiWeather(WeatherEntity): """Representation of a weather entity.""" _attr_attribution = "Swedish weather institute (SMHI)" - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_visibility_unit = LENGTH_KILOMETERS - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND - _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_visibility_unit = UnitOfLength.KILOMETERS + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_native_pressure_unit = UnitOfPressure.HPA _attr_has_entity_name = True @@ -158,7 +158,7 @@ class SmhiWeather(WeatherEntity): if self._forecasts: wind_gust = SpeedConverter.convert( self._forecasts[0].wind_gust, - SPEED_METERS_PER_SECOND, + UnitOfSpeed.METERS_PER_SECOND, self._wind_speed_unit, ) return { diff --git a/homeassistant/components/sms/translations/sk.json b/homeassistant/components/sms/translations/sk.json new file mode 100644 index 00000000000..848d127c43a --- /dev/null +++ b/homeassistant/components/sms/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "baud_speed": "R\u00fdchlos\u0165 prenosu", + "device": "Zariadenie" + }, + "title": "Pripojte sa k modemu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 01471b13bc7..d4619fa3b3a 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -90,6 +90,14 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Activate Snips component.""" + # Make sure MQTT is available and the entry is loaded + if not hass.config_entries.async_entries( + mqtt.DOMAIN + ) or not await hass.config_entries.async_wait_component( + hass.config_entries.async_entries(mqtt.DOMAIN)[0] + ): + _LOGGER.error("MQTT integration is not available") + return False async def async_set_feedback(site_ids, state): """Set Feedback sound state.""" diff --git a/homeassistant/components/snooz/translations/cs.json b/homeassistant/components/snooz/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/snooz/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/el.json b/homeassistant/components/snooz/translations/el.json new file mode 100644 index 00000000000..034770f7ce0 --- /dev/null +++ b/homeassistant/components/snooz/translations/el.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03b2\u03ac\u03bb\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2. \n\n ### \u03a0\u03ce\u03c2 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03ad\u03bb\u03b8\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2\n 1. \u0391\u03bd\u03b1\u03b3\u03ba\u03b1\u03c3\u03c4\u03b9\u03ba\u03ae \u03ad\u03be\u03bf\u03b4\u03bf\u03c2 \u03b1\u03c0\u03cc \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac SNOOZ.\n 2. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03ba\u03c1\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03b1\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03c4\u03b1\u03bd \u03b1\u03c1\u03c7\u03af\u03c3\u03bf\u03c5\u03bd \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03bf\u03c5\u03bd \u03c4\u03b1 \u03c6\u03ce\u03c4\u03b1 (\u03c0\u03b5\u03c1\u03af\u03c0\u03bf\u03c5 5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)." + }, + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "pairing_timeout": { + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03b9\u03c3\u03ae\u03bb\u03b8\u03b5 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac. \n\n ### \u0391\u03bd\u03c4\u03b9\u03bc\u03b5\u03c4\u03ce\u03c0\u03b9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b2\u03bb\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd\n 1. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac.\n 2. \u0391\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac." + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/he.json b/homeassistant/components/snooz/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/snooz/translations/he.json +++ b/homeassistant/components/snooz/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/snooz/translations/hr.json b/homeassistant/components/snooz/translations/hr.json new file mode 100644 index 00000000000..ff7217c5352 --- /dev/null +++ b/homeassistant/components/snooz/translations/hr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "already_in_progress": "Konfiguracije je ve\u0107 u tijeku", + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u017delite li postaviti {name}?" + }, + "user": { + "data": { + "address": "Ure\u0111aj" + }, + "description": "Odaberite ure\u0111aj za postavljanje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/sk.json b/homeassistant/components/snooz/translations/sk.json new file mode 100644 index 00000000000..b121bbc35a3 --- /dev/null +++ b/homeassistant/components/snooz/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/sk.json b/homeassistant/components/solaredge/translations/sk.json index 7c6659c2e0b..c954ca5b347 100644 --- a/homeassistant/components/solaredge/translations/sk.json +++ b/homeassistant/components/solaredge/translations/sk.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "could_not_connect": "Nepodarilo sa pripoji\u0165 k rozhraniu solaredge API", "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" }, "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" + "api_key": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov tejto in\u0161tal\u00e1cie" } } } diff --git a/homeassistant/components/solarlog/translations/sk.json b/homeassistant/components/solarlog/translations/sk.json new file mode 100644 index 00000000000..35dd47181e4 --- /dev/null +++ b/homeassistant/components/solarlog/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/sk.json b/homeassistant/components/solax/translations/sk.json index 892b8b2cd91..ba1e9330c9b 100644 --- a/homeassistant/components/solax/translations/sk.json +++ b/homeassistant/components/solax/translations/sk.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { + "ip_address": "IP adresa", + "password": "Heslo", "port": "Port" } } diff --git a/homeassistant/components/soma/translations/sk.json b/homeassistant/components/soma/translations/sk.json index 91a46a2787f..5afd4e7cbb8 100644 --- a/homeassistant/components/soma/translations/sk.json +++ b/homeassistant/components/soma/translations/sk.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_setup": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "connection_error": "Nepodarilo sa pripoji\u0165" + }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" }, "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/somfy/translations/sk.json b/homeassistant/components/somfy/translations/sk.json index c19b1a0b70c..bd4f9111c68 100644 --- a/homeassistant/components/somfy/translations/sk.json +++ b/homeassistant/components/somfy/translations/sk.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json index 5ee98a5d46c..fdc3b2a9b4c 100644 --- a/homeassistant/components/somfy_mylink/translations/bg.json +++ b/homeassistant/components/somfy_mylink/translations/bg.json @@ -11,5 +11,10 @@ } } } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/sk.json b/homeassistant/components/somfy_mylink/translations/sk.json index 1145b3bb9f8..8fccd88d983 100644 --- a/homeassistant/components/somfy_mylink/translations/sk.json +++ b/homeassistant/components/somfy_mylink/translations/sk.json @@ -1,14 +1,28 @@ { "config": { - "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { - "port": "Port" - } + "host": "Hostite\u013e", + "port": "Port", + "system_id": "ID syst\u00e9mu" + }, + "description": "Syst\u00e9mov\u00e9 ID je mo\u017en\u00e9 z\u00edska\u0165 v aplik\u00e1cii MyLink v \u010dasti Integr\u00e1cia v\u00fdberom akejko\u013evek inej ne\u017e cloudovej slu\u017eby." } } + }, + "options": { + "abort": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + } } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index 05d96cf621c..ac4dc5ea4bf 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/sonarr/translations/sk.json b/homeassistant/components/sonarr/translations/sk.json index 64731388e98..23b61b9c00f 100644 --- a/homeassistant/components/sonarr/translations/sk.json +++ b/homeassistant/components/sonarr/translations/sk.json @@ -1,15 +1,33 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, + "flow_title": "{name}", "step": { + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" + "api_key": "API k\u013e\u00fa\u010d", + "url": "URL", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "wanted_max_items": "Maxim\u00e1lny po\u010det po\u017eadovan\u00fdch polo\u017eiek na zobrazenie" } } } diff --git a/homeassistant/components/songpal/translations/he.json b/homeassistant/components/songpal/translations/he.json index a8c8d1d0294..a90c9b90858 100644 --- a/homeassistant/components/songpal/translations/he.json +++ b/homeassistant/components/songpal/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05de\u05db\u05e9\u05d9\u05e8 \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/songpal/translations/sk.json b/homeassistant/components/songpal/translations/sk.json new file mode 100644 index 00000000000..a81b795afc4 --- /dev/null +++ b/homeassistant/components/songpal/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name} ({host})", + "step": { + "init": { + "description": "Chcete nastavi\u0165 {name} ({host})?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index 24233b1316f..ab34457e3fc 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -43,11 +43,11 @@ DURATION_SECONDS = "duration_in_s" POSITION_SECONDS = "position_in_s" -def _timespan_secs(timespan: str | None) -> None | float: +def _timespan_secs(timespan: str | None) -> None | int: """Parse a time-span into number of seconds.""" if timespan in UNAVAILABLE_VALUES: return None - return time_period_str(timespan).total_seconds() # type: ignore[arg-type] + return int(time_period_str(timespan).total_seconds()) # type: ignore[arg-type] class SonosMedia: @@ -73,7 +73,7 @@ class SonosMedia: self.title: str | None = None self.uri: str | None = None - self.position: float | None = None + self.position: int | None = None self.position_updated_at: datetime.datetime | None = None def clear(self) -> None: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4195f284ffe..bd50a090175 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -323,7 +323,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @property def media_position(self) -> int | None: """Position of current playing media in seconds.""" - return int(self.media.position) if self.media.position else None + return self.media.position @property def media_position_updated_at(self) -> datetime.datetime | None: diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index acf33ea34aa..bbeda6edb63 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -16,6 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_change from .const import ( DATA_SONOS, @@ -91,6 +92,8 @@ FEATURE_ICONS = { ATTR_TOUCH_CONTROLS: "mdi:gesture-tap", } +WEEKEND_DAYS = (0, 6) + async def async_setup_entry( hass: HomeAssistant, @@ -233,6 +236,17 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): ) ) + async def async_write_state_daily(now: datetime.datetime) -> None: + """Update alarm state attributes each calendar day.""" + _LOGGER.debug("Updating state attributes for %s", self.name) + self.async_write_ha_state() + + self.async_on_remove( + async_track_time_change( + self.hass, async_write_state_daily, hour=0, minute=0, second=0 + ) + ) + @property def alarm(self) -> Alarm: """Return the alarm instance.""" @@ -304,14 +318,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): def _is_today(self) -> bool: """Return whether this alarm is scheduled for today.""" recurrence = self.alarm.recurrence - timestr = int(datetime.datetime.today().strftime("%w")) + daynum = int(datetime.datetime.today().strftime("%w")) return ( - bool(recurrence[:2] == "ON" and str(timestr) in recurrence) - or bool(recurrence == "DAILY") - or bool(recurrence == "WEEKDAYS" and int(timestr) not in [0, 7]) - or bool(recurrence == "ONCE") - or bool(recurrence == "WEEKDAYS" and int(timestr) not in [0, 7]) - or bool(recurrence == "WEEKENDS" and int(timestr) not in range(1, 7)) + recurrence in ("DAILY", "ONCE") + or (recurrence == "WEEKENDS" and daynum in WEEKEND_DAYS) + or (recurrence == "WEEKDAYS" and daynum not in WEEKEND_DAYS) + or (recurrence.startswith("ON_") and str(daynum) in recurrence) ) @property diff --git a/homeassistant/components/sonos/translations/he.json b/homeassistant/components/sonos/translations/he.json index 64824b942ec..5e29646acfb 100644 --- a/homeassistant/components/sonos/translations/he.json +++ b/homeassistant/components/sonos/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "not_sonos_device": "\u05d4\u05ea\u05e7\u05df \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05d4\u05ea\u05e7\u05df Sonos", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, diff --git a/homeassistant/components/sonos/translations/sk.json b/homeassistant/components/sonos/translations/sk.json new file mode 100644 index 00000000000..99798036ffd --- /dev/null +++ b/homeassistant/components/sonos/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json index 379516e31be..8ddf597c477 100644 --- a/homeassistant/components/soundtouch/translations/de.json +++ b/homeassistant/components/soundtouch/translations/de.json @@ -13,8 +13,8 @@ } }, "zeroconf_confirm": { - "description": "Du bist im Begriff, das SoundTouch-Ger\u00e4t mit dem Namen `{name}` zu Home Assistant hinzuzuf\u00fcgen.", - "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" + "description": "Du bist im Begriff, das SoundTouch Ger\u00e4t mit dem Namen `{name}` zu Home Assistant hinzuzuf\u00fcgen.", + "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch Ger\u00e4ts" } } }, diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json index cabbb3a6224..f267a63ba1a 100644 --- a/homeassistant/components/soundtouch/translations/id.json +++ b/homeassistant/components/soundtouch/translations/id.json @@ -13,7 +13,7 @@ } }, "zeroconf_confirm": { - "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", + "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", "title": "Konfirmasi penambahan perangkat Bose SoundTouch" } } diff --git a/homeassistant/components/soundtouch/translations/sk.json b/homeassistant/components/soundtouch/translations/sk.json new file mode 100644 index 00000000000..c8e33ffce5e --- /dev/null +++ b/homeassistant/components/soundtouch/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/bg.json b/homeassistant/components/speedtestdotnet/translations/bg.json index 31163643662..0e175ee9902 100644 --- a/homeassistant/components/speedtestdotnet/translations/bg.json +++ b/homeassistant/components/speedtestdotnet/translations/bg.json @@ -9,18 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 Speedtest \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" - } - } - }, - "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 Speedtest \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/ca.json b/homeassistant/components/speedtestdotnet/translations/ca.json index fa29c792992..c1c5dda71cf 100644 --- a/homeassistant/components/speedtestdotnet/translations/ca.json +++ b/homeassistant/components/speedtestdotnet/translations/ca.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `homeassistant.update_entity` amb un 'entity_id' objectiu o 'target' de Speedtest. Despr\u00e9s, fes clic a ENVIA per marcar aquest problema com a resolt.", - "title": "El servei speedtest est\u00e0 sent eliminat" - } - } - }, - "title": "El servei speedtest est\u00e0 sent eliminat" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index f33c088998f..81910cb9c70 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst \"homeassistant.update_entity\" mit einer Speedtest-Entity_id zu verwenden. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", - "title": "Der Speedtest-Dienst wird entfernt" - } - } - }, - "title": "Der Speedtest-Dienst wird entfernt" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/el.json b/homeassistant/components/speedtestdotnet/translations/el.json index 1b4637fd76a..25b5e23ab69 100644 --- a/homeassistant/components/speedtestdotnet/translations/el.json +++ b/homeassistant/components/speedtestdotnet/translations/el.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `homeassistant.update_entity` \u03bc\u03b5 \u03ad\u03bd\u03b1 target Speedtest entity_id. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1 \u03c9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03c5\u03bc\u03ad\u03bd\u03bf.", - "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 speedtest \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - } - }, - "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 speedtest \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/en.json b/homeassistant/components/speedtestdotnet/translations/en.json index 623aee3b2e8..eab480073bc 100644 --- a/homeassistant/components/speedtestdotnet/translations/en.json +++ b/homeassistant/components/speedtestdotnet/translations/en.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this service to instead use the `homeassistant.update_entity` service with a target Speedtest entity_id. Then, click SUBMIT below to mark this issue as resolved.", - "title": "The speedtest service is being removed" - } - } - }, - "title": "The speedtest service is being removed" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index f439b2e1079..9ba5fcbd4bb 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `homeassistant.update_entity` con un entity_id de Speedtest como objetivo. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", - "title": "Se va a eliminar el servicio speedtest" - } - } - }, - "title": "Se va a eliminar el servicio speedtest" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/et.json b/homeassistant/components/speedtestdotnet/translations/et.json index abfe42c3cfa..ac1915e760a 100644 --- a/homeassistant/components/speedtestdotnet/translations/et.json +++ b/homeassistant/components/speedtestdotnet/translations/et.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "V\u00e4rskenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et kasutada selle asemel teenust \"homeassistant.update_entity\" sihtv\u00e4\u00e4rtusega Speedtest entity_id. Seej\u00e4rel kl\u00f5psa selle probleemi lahendatuks m\u00e4rkimiseks allpool nuppu ESITA.", - "title": "Speedtest teenus eemaldatakse" - } - } - }, - "title": "Speedtest teenus eemaldatakse" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json index a89b7e89977..d2efebd0eb1 100644 --- a/homeassistant/components/speedtestdotnet/translations/fr.json +++ b/homeassistant/components/speedtestdotnet/translations/fr.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Modifiez tout script ou automatisation utilisant ce service afin qu'ils utilisent \u00e0 la place le service `homeassistant.update_entity` avec l'entity_id d'un Speedtest pour cible. Cliquez ensuite sur VALIDER ci-dessous afin de marquer ce probl\u00e8me comme r\u00e9solu.", - "title": "Le service speedtest sera bient\u00f4t supprim\u00e9" - } - } - }, - "title": "Le service speedtest sera bient\u00f4t supprim\u00e9" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index f99db19fe4a..c223e8b9376 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a `homeassistant.update_entity` szolg\u00e1ltat\u00e1st haszn\u00e1lja a Speedtest entity_id azonos\u00edt\u00f3val. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", - "title": "A speedtest szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - } - }, - "title": "A speedtest szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/id.json b/homeassistant/components/speedtestdotnet/translations/id.json index 8a5e3711d93..f609c3d384a 100644 --- a/homeassistant/components/speedtestdotnet/translations/id.json +++ b/homeassistant/components/speedtestdotnet/translations/id.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `homeassistant.update_entity` dengan target ID entitas Speedtest. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", - "title": "Layanan speedtest dalam proses penghapusan" - } - } - }, - "title": "Layanan speedtest dalam proses penghapusan" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/it.json b/homeassistant/components/speedtestdotnet/translations/it.json index c9e02758bb2..07615fe093c 100644 --- a/homeassistant/components/speedtestdotnet/translations/it.json +++ b/homeassistant/components/speedtestdotnet/translations/it.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `homeassistant.update_entity` con un entity_id Speedtest di destinazione. Quindi, fai clic su INVIA di seguito per contrassegnare questo problema come risolto.", - "title": "Il servizio speedtest \u00e8 stato rimosso" - } - } - }, - "title": "Il servizio speedtest \u00e8 stato rimosso" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index c09a2a6032a..40f592b2c46 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b\u3059\u3079\u3066\u306e\u81ea\u52d5\u5316\u307e\u305f\u306f\u30b9\u30af\u30ea\u30d7\u30c8\u3092\u66f4\u65b0\u3057\u3066\u3001\u4ee3\u308f\u308a\u306b\u30bf\u30fc\u30b2\u30c3\u30c8 Speedtest entity_id \u3067\u300chomeassistant.update_entity\u300d\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002\u6b21\u306b\u3001\u4e0b\u306e [\u9001\u4fe1] \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u6e08\u307f\u3068\u3057\u3066\u30de\u30fc\u30af\u3057\u307e\u3059\u3002", - "title": "speedtest\u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } - } - }, - "title": "speedtest\u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json index 127541b5926..01909d39f06 100644 --- a/homeassistant/components/speedtestdotnet/translations/no.json +++ b/homeassistant/components/speedtestdotnet/translations/no.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke `homeassistant.update_entity`-tjenesten med en m\u00e5l Speedtest-entity_id. Klikk deretter SEND nedenfor for \u00e5 merke dette problemet som l\u00f8st.", - "title": "Speedtest-tjenesten fjernes" - } - } - }, - "title": "Speedtest-tjenesten blir fjernet" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/pl.json b/homeassistant/components/speedtestdotnet/translations/pl.json index 237d05ffd9c..b06d7cdd285 100644 --- a/homeassistant/components/speedtestdotnet/translations/pl.json +++ b/homeassistant/components/speedtestdotnet/translations/pl.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi \u201ehomeassistant.update_entity\u201d z encj\u0105 docelow\u0105 Speedtest. Nast\u0119pnie kliknij ZATWIERD\u0179 poni\u017cej, aby oznaczy\u0107 ten problem jako rozwi\u0105zany.", - "title": "Us\u0142uga speedtest zostanie usuni\u0119ta" - } - } - }, - "title": "Us\u0142uga speedtest zostanie usuni\u0119ta" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/pt-BR.json b/homeassistant/components/speedtestdotnet/translations/pt-BR.json index 7a159970a7f..739b3b41875 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt-BR.json +++ b/homeassistant/components/speedtestdotnet/translations/pt-BR.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `homeassistant.update_entity` com um ID de entidade do Speedtest. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", - "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" - } - } - }, - "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/pt.json b/homeassistant/components/speedtestdotnet/translations/pt.json index b0c105531f2..c299020ce9a 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt.json +++ b/homeassistant/components/speedtestdotnet/translations/pt.json @@ -8,18 +8,5 @@ "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" } } - }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `homeassistant.update_entity` com um ID de entidade do Speedtest de destino. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", - "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" - } - } - }, - "title": "O servi\u00e7o speedtest est\u00e1 sendo removido" - } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/ru.json b/homeassistant/components/speedtestdotnet/translations/ru.json index 45b3a7da55a..3ffcd10bf31 100644 --- a/homeassistant/components/speedtestdotnet/translations/ru.json +++ b/homeassistant/components/speedtestdotnet/translations/ru.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u043b\u0443\u0436\u0431\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `homeassistant.update_entity` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c Speedtest. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", - "title": "\u0421\u043b\u0443\u0436\u0431\u0430 speedtest \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - } - } - }, - "title": "\u0421\u043b\u0443\u0436\u0431\u0430 speedtest \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/sk.json b/homeassistant/components/speedtestdotnet/translations/sk.json new file mode 100644 index 00000000000..a74820e4d4f --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "Zak\u00e1za\u0165 automatick\u00fa aktualiz\u00e1ciu", + "scan_interval": "Frekvencia aktualiz\u00e1cie (min\u00faty)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/sv.json b/homeassistant/components/speedtestdotnet/translations/sv.json index 81e07b927c7..cde9095fba8 100644 --- a/homeassistant/components/speedtestdotnet/translations/sv.json +++ b/homeassistant/components/speedtestdotnet/translations/sv.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Uppdatera eventuella automatiseringar eller skript som anv\u00e4nder den h\u00e4r tj\u00e4nsten f\u00f6r att ist\u00e4llet anv\u00e4nda tj\u00e4nsten `homeassistant.update_entity` med ett m\u00e5l Speedtest-entity_id. Klicka sedan p\u00e5 SKICKA nedan f\u00f6r att markera problemet som l\u00f6st.", - "title": "Tj\u00e4nsten speedtest tas bort" - } - } - }, - "title": "Tj\u00e4nsten speedtest tas bort" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json index d096c2c264d..5becafdf153 100644 --- a/homeassistant/components/speedtestdotnet/translations/tr.json +++ b/homeassistant/components/speedtestdotnet/translations/tr.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \"homeassistant.update_entity\" hizmetini bir hedef Speedtest entity_id ile kullanacak \u015fekilde g\u00fcncelleyin. Ard\u0131ndan, bu sorunu \u00e7\u00f6z\u00fcld\u00fc olarak i\u015faretlemek i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'i t\u0131klay\u0131n.", - "title": "Speedtest hizmeti kald\u0131r\u0131l\u0131yor" - } - } - }, - "title": "Speedtest hizmeti kald\u0131r\u0131l\u0131yor" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index 3d12b9454d0..43d30d4aeb8 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -9,19 +9,6 @@ } } }, - "issues": { - "deprecated_service": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19 entity_id \u70ba Speedtest \u4e4b `homeassistant.update_entity` \u670d\u52d9\uff0c\u7136\u5f8c\u9ede\u9078\u50b3\u9001\u4ee5\u6a19\u793a\u554f\u984c\u5df2\u89e3\u6c7a\u3002", - "title": "Seedtest \u670d\u52d9\u5373\u5c07\u79fb\u9664" - } - } - }, - "title": "Speedtest \u670d\u52d9\u5373\u5c07\u79fb\u9664" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 6c02e8485c4..111c1e377cf 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -64,7 +64,7 @@ class SpiderThermostat(ClimateEntity): ) @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" if self.thermostat.has_fan_mode: return ( diff --git a/homeassistant/components/spider/translations/sk.json b/homeassistant/components/spider/translations/sk.json index 5ada995aa6e..52f9f59ae60 100644 --- a/homeassistant/components/spider/translations/sk.json +++ b/homeassistant/components/spider/translations/sk.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 0ce71f371df..be662e969f2 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.20.0"], + "requirements": ["spotipy==2.21.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["application_credentials"], "codeowners": ["@frenck"], diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index 9d48e7b58e2..866590d00cb 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -22,7 +22,7 @@ "issues": { "removed_yaml": { "description": "Die Konfiguration von Spotify \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Spotify-YAML-Konfiguration wurde entfernt" + "title": "Die Spotify YAML-Konfiguration wurde entfernt" } }, "system_health": { diff --git a/homeassistant/components/spotify/translations/sk.json b/homeassistant/components/spotify/translations/sk.json index 63ae49fe727..dc3096178d5 100644 --- a/homeassistant/components/spotify/translations/sk.json +++ b/homeassistant/components/spotify/translations/sk.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL." + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9 v slu\u017ebe Spotify." + }, "step": { "pick_implementation": { "title": "Vyberte met\u00f3du overenia" diff --git a/homeassistant/components/sql/translations/sk.json b/homeassistant/components/sql/translations/sk.json new file mode 100644 index 00000000000..1db0d01443c --- /dev/null +++ b/homeassistant/components/sql/translations/sk.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "db_url_invalid": "Adresa URL datab\u00e1zy je neplatn\u00e1", + "query_invalid": "SQL neplatn\u00e1 po\u017eiadavka" + }, + "step": { + "user": { + "data": { + "column": "St\u013apec", + "db_url": "URL datab\u00e1zy", + "name": "N\u00e1zov", + "unit_of_measurement": "Mern\u00e1 jednotka" + }, + "data_description": { + "db_url": "Adresa URL datab\u00e1zy, nechajte pr\u00e1zdnu, ak chcete pou\u017ei\u0165 predvolen\u00fa datab\u00e1zu HA", + "unit_of_measurement": "Jednotka miery (volite\u013en\u00e9)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "Adresa URL datab\u00e1zy je neplatn\u00e1", + "query_invalid": "SQL neplatn\u00e1 po\u017eiadavka" + }, + "step": { + "init": { + "data": { + "column": "St\u013apec", + "db_url": "URL datab\u00e1zy", + "name": "N\u00e1zov", + "unit_of_measurement": "Mern\u00e1 jednotka" + }, + "data_description": { + "db_url": "Adresa URL datab\u00e1zy, nechajte pr\u00e1zdnu, ak chcete pou\u017ei\u0165 predvolen\u00fa datab\u00e1zu HA", + "unit_of_measurement": "Jednotka miery (volite\u013en\u00e9)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/sk.json b/homeassistant/components/squeezebox/translations/sk.json index 85b770fe2ed..59f8b706503 100644 --- a/homeassistant/components/squeezebox/translations/sk.json +++ b/homeassistant/components/squeezebox/translations/sk.json @@ -1,12 +1,27 @@ { "config": { - "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{host}", "step": { "edit": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Upravte inform\u00e1cie o pripojen\u00ed" + }, + "user": { + "data": { + "host": "Hostite\u013e" } } } diff --git a/homeassistant/components/srp_energy/translations/sk.json b/homeassistant/components/srp_energy/translations/sk.json index 5ada995aa6e..23ac0bfe739 100644 --- a/homeassistant/components/srp_energy/translations/sk.json +++ b/homeassistant/components/srp_energy/translations/sk.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_account": "ID \u00fa\u010dtu by malo by\u0165 9-miestne \u010d\u00edslo", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "id": "ID \u00fa\u010dtu", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 3b30146e756..d532dc8f292 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.32.2"], + "requirements": ["async-upnp-client==0.32.3"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index e01807ba702..6dadfdbd3ea 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -1,6 +1,5 @@ """StarLine device tracker.""" -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/starline/translations/sk.json b/homeassistant/components/starline/translations/sk.json new file mode 100644 index 00000000000..79b2db36bb4 --- /dev/null +++ b/homeassistant/components/starline/translations/sk.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "error_auth_mfa": "Nespr\u00e1vny k\u00f3d", + "error_auth_user": "Nespr\u00e1vne u\u017e\u00edvate\u013esk\u00e9 meno alebo heslo" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID aplik\u00e1cie" + } + }, + "auth_captcha": { + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS k\u00f3d" + }, + "description": "Zadajte k\u00f3d odoslan\u00fd na telef\u00f3n {phone_number}", + "title": "Dvojfaktorov\u00e1 autoriz\u00e1cia" + }, + "auth_user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index cfc093c7762..64e967604c1 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -34,11 +34,10 @@ from homeassistant.core import ( Event, HomeAssistant, State, - async_get_hass, callback, split_entity_id, ) -from homeassistant.helpers import config_validation as cv, issue_registry +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_point_in_utc_time, @@ -78,7 +77,7 @@ STAT_DISTANCE_ABSOLUTE = "distance_absolute" STAT_MEAN = "mean" STAT_MEDIAN = "median" STAT_NOISINESS = "noisiness" -STAT_QUANTILES = "quantiles" +STAT_PERCENTILE = "percentile" STAT_STANDARD_DEVIATION = "standard_deviation" STAT_SUM = "sum" STAT_SUM_DIFFERENCES = "sum_differences" @@ -107,7 +106,7 @@ STATS_NUMERIC_SUPPORT = { STAT_MEAN, STAT_MEDIAN, STAT_NOISINESS, - STAT_QUANTILES, + STAT_PERCENTILE, STAT_STANDARD_DEVIATION, STAT_SUM, STAT_SUM_DIFFERENCES, @@ -135,7 +134,6 @@ STATS_NOT_A_NUMBER = { STAT_DATETIME_OLDEST, STAT_DATETIME_VALUE_MAX, STAT_DATETIME_VALUE_MIN, - STAT_QUANTILES, } STATS_DATETIME = { @@ -157,6 +155,7 @@ STAT_NUMERIC_RETAIN_UNIT = { STAT_MEAN, STAT_MEDIAN, STAT_NOISINESS, + STAT_PERCENTILE, STAT_STANDARD_DEVIATION, STAT_SUM, STAT_SUM_DIFFERENCES, @@ -177,37 +176,16 @@ CONF_STATE_CHARACTERISTIC = "state_characteristic" CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size" CONF_MAX_AGE = "max_age" CONF_PRECISION = "precision" -CONF_QUANTILE_INTERVALS = "quantile_intervals" -CONF_QUANTILE_METHOD = "quantile_method" +CONF_PERCENTILE = "percentile" -DEFAULT_NAME = "Stats" +DEFAULT_NAME = "Statistical characteristic" DEFAULT_PRECISION = 2 -DEFAULT_QUANTILE_INTERVALS = 4 -DEFAULT_QUANTILE_METHOD = "exclusive" ICON = "mdi:calculator" def valid_state_characteristic_configuration(config: dict[str, Any]) -> dict[str, Any]: """Validate that the characteristic selected is valid for the source sensor type, throw if it isn't.""" is_binary = split_entity_id(config[CONF_ENTITY_ID])[0] == BINARY_SENSOR_DOMAIN - - if config.get(CONF_STATE_CHARACTERISTIC) is None: - config[CONF_STATE_CHARACTERISTIC] = STAT_COUNT if is_binary else STAT_MEAN - issue_registry.async_create_issue( - hass=async_get_hass(), - domain=DOMAIN, - issue_id=f"{config[CONF_ENTITY_ID]}_default_characteristic", - breaks_in_ha_version="2022.12.0", - is_fixable=False, - severity=issue_registry.IssueSeverity.WARNING, - translation_key="deprecation_warning_characteristic", - translation_placeholders={ - "entity": config[CONF_NAME], - "characteristic": config[CONF_STATE_CHARACTERISTIC], - }, - learn_more_url="https://github.com/home-assistant/core/pull/60402", - ) - characteristic = cast(str, config[CONF_STATE_CHARACTERISTIC]) if (is_binary and characteristic not in STATS_BINARY_SUPPORT) or ( not is_binary and characteristic not in STATS_NUMERIC_SUPPORT @@ -221,20 +199,14 @@ def valid_state_characteristic_configuration(config: dict[str, Any]) -> dict[str def valid_boundary_configuration(config: dict[str, Any]) -> dict[str, Any]: - """Validate that sampling_size, max_age, or both are provided.""" + """Validate that max_age, sampling_size, or both are provided.""" - if config.get(CONF_SAMPLES_MAX_BUFFER_SIZE) is None: - config[CONF_SAMPLES_MAX_BUFFER_SIZE] = 20 - issue_registry.async_create_issue( - hass=async_get_hass(), - domain=DOMAIN, - issue_id=f"{config[CONF_ENTITY_ID]}_invalid_boundary_config", - breaks_in_ha_version="2022.12.0", - is_fixable=False, - severity=issue_registry.IssueSeverity.WARNING, - translation_key="deprecation_warning_size", - translation_placeholders={"entity": config[CONF_NAME]}, - learn_more_url="https://github.com/home-assistant/core/pull/69700", + if ( + config.get(CONF_SAMPLES_MAX_BUFFER_SIZE) is None + and config.get(CONF_MAX_AGE) is None + ): + raise vol.RequiredFieldInvalid( + "The sensor configuration must provide 'max_age' and/or 'sampling_size'" ) return config @@ -244,15 +216,14 @@ _PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend( vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_STATE_CHARACTERISTIC): cv.string, - vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): vol.Coerce(int), + vol.Required(CONF_STATE_CHARACTERISTIC): cv.string, + vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), vol.Optional(CONF_MAX_AGE): cv.time_period, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), - vol.Optional( - CONF_QUANTILE_INTERVALS, default=DEFAULT_QUANTILE_INTERVALS - ): vol.All(vol.Coerce(int), vol.Range(min=2)), - vol.Optional(CONF_QUANTILE_METHOD, default=DEFAULT_QUANTILE_METHOD): vol.In( - ["exclusive", "inclusive"] + vol.Optional(CONF_PERCENTILE, default=50): vol.All( + vol.Coerce(int), vol.Range(min=1, max=99) ), } ) @@ -280,11 +251,10 @@ async def async_setup_platform( name=config[CONF_NAME], unique_id=config.get(CONF_UNIQUE_ID), state_characteristic=config[CONF_STATE_CHARACTERISTIC], - samples_max_buffer_size=config[CONF_SAMPLES_MAX_BUFFER_SIZE], + samples_max_buffer_size=config.get(CONF_SAMPLES_MAX_BUFFER_SIZE), samples_max_age=config.get(CONF_MAX_AGE), precision=config[CONF_PRECISION], - quantile_intervals=config[CONF_QUANTILE_INTERVALS], - quantile_method=config[CONF_QUANTILE_METHOD], + percentile=config[CONF_PERCENTILE], ) ], update_before_add=True, @@ -300,11 +270,10 @@ class StatisticsSensor(SensorEntity): name: str, unique_id: str | None, state_characteristic: str, - samples_max_buffer_size: int, + samples_max_buffer_size: int | None, samples_max_age: timedelta | None, precision: int, - quantile_intervals: int, - quantile_method: Literal["exclusive", "inclusive"], + percentile: int, ) -> None: """Initialize the Statistics sensor.""" self._attr_icon: str = ICON @@ -316,21 +285,17 @@ class StatisticsSensor(SensorEntity): split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN ) self._state_characteristic: str = state_characteristic - self._samples_max_buffer_size: int = samples_max_buffer_size + self._samples_max_buffer_size: int | None = samples_max_buffer_size self._samples_max_age: timedelta | None = samples_max_age self._precision: int = precision - self._quantile_intervals: int = quantile_intervals - self._quantile_method: Literal["exclusive", "inclusive"] = quantile_method + self._percentile: int = percentile self._value: StateType | datetime = None self._unit_of_measurement: str | None = None self._available: bool = False + self.states: deque[float | bool] = deque(maxlen=self._samples_max_buffer_size) self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size) - self.attributes: dict[str, StateType] = { - STAT_AGE_COVERAGE_RATIO: None, - STAT_BUFFER_USAGE_RATIO: None, - STAT_SOURCE_VALUE_VALID: None, - } + self.attributes: dict[str, StateType] = {} self._state_characteristic_fn: Callable[[], StateType | datetime] if self.is_binary: @@ -505,11 +470,8 @@ class StatisticsSensor(SensorEntity): self._update_value() # If max_age is set, ensure to update again after the defined interval. - next_to_purge_timestamp = self._next_to_purge_timestamp() - if next_to_purge_timestamp: - _LOGGER.debug( - "%s: scheduling update at %s", self.entity_id, next_to_purge_timestamp - ) + if timestamp := self._next_to_purge_timestamp(): + _LOGGER.debug("%s: scheduling update at %s", self.entity_id, timestamp) if self._update_listener: self._update_listener() self._update_listener = None @@ -522,7 +484,7 @@ class StatisticsSensor(SensorEntity): self._update_listener = None self._update_listener = async_track_point_in_utc_time( - self.hass, _scheduled_update, next_to_purge_timestamp + self.hass, _scheduled_update, timestamp ) def _fetch_states_from_database(self) -> list[State]: @@ -572,18 +534,20 @@ class StatisticsSensor(SensorEntity): def _update_attributes(self) -> None: """Calculate and update the various attributes.""" - self.attributes[STAT_BUFFER_USAGE_RATIO] = round( - len(self.states) / self._samples_max_buffer_size, 2 - ) - - if len(self.states) >= 1 and self._samples_max_age is not None: - self.attributes[STAT_AGE_COVERAGE_RATIO] = round( - (self.ages[-1] - self.ages[0]).total_seconds() - / self._samples_max_age.total_seconds(), - 2, + if self._samples_max_buffer_size is not None: + self.attributes[STAT_BUFFER_USAGE_RATIO] = round( + len(self.states) / self._samples_max_buffer_size, 2 ) - else: - self.attributes[STAT_AGE_COVERAGE_RATIO] = None + + if self._samples_max_age is not None: + if len(self.states) >= 1: + self.attributes[STAT_AGE_COVERAGE_RATIO] = round( + (self.ages[-1] - self.ages[0]).total_seconds() + / self._samples_max_age.total_seconds(), + 2, + ) + else: + self.attributes[STAT_AGE_COVERAGE_RATIO] = None def _update_value(self) -> None: """Front to call the right statistical characteristics functions. @@ -700,18 +664,10 @@ class StatisticsSensor(SensorEntity): return cast(float, self._stat_sum_differences()) / (len(self.states) - 1) return None - def _stat_quantiles(self) -> StateType: - if len(self.states) > self._quantile_intervals: - return str( - [ - round(quantile, self._precision) - for quantile in statistics.quantiles( - self.states, - n=self._quantile_intervals, - method=self._quantile_method, - ) - ] - ) + def _stat_percentile(self) -> StateType: + if len(self.states) >= 2: + percentiles = statistics.quantiles(self.states, n=100, method="exclusive") + return percentiles[self._percentile - 1] return None def _stat_standard_deviation(self) -> StateType: diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json deleted file mode 100644 index 0cca71f172f..00000000000 --- a/homeassistant/components/statistics/strings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "issues": { - "deprecation_warning_characteristic": { - "description": "The configuration parameter `state_characteristic` of the statistics integration will become mandatory.\n\nPlease add `state_characteristic: {characteristic}` to the configuration of sensor `{entity}` to keep the current behavior.\n\nRead the documentation of the statistics integration for further details: https://www.home-assistant.io/integrations/statistics/", - "title": "Mandatory 'state_characteristic' assumed for a Statistics entity" - }, - "deprecation_warning_size": { - "description": "The configuration parameter `sampling_size` of the statistics integration defaulted to the value 20 so far, which will change.\n\nPlease check the configuration for sensor `{entity}` and add suited boundaries, e.g., `sampling_size: 20` to keep the current behavior. The configuration of the statistics integration will become more flexible with version 2022.12.0 and accept either `sampling_size` or `max_age`, or both settings. The request above prepares your configuration for this otherwise breaking change.\n\nRead the documentation of the statistics integration for further details: https://www.home-assistant.io/integrations/statistics/", - "title": "Implicit 'sampling_size' assumed for a Statistics entity" - } - } -} diff --git a/homeassistant/components/statistics/translations/de.json b/homeassistant/components/statistics/translations/de.json index f29b161ec39..54e95c647c1 100644 --- a/homeassistant/components/statistics/translations/de.json +++ b/homeassistant/components/statistics/translations/de.json @@ -5,7 +5,7 @@ "title": "Obligatorisches 'state_characteristic' wird f\u00fcr eine Statistikentit\u00e4t angenommen" }, "deprecation_warning_size": { - "description": "Der Konfigurationsparameter `sampling_size` der Statistikintegration war bisher standardm\u00e4\u00dfig auf den Wert 20 eingestellt, was sich \u00e4ndern wird.\n\nBitte \u00fcberpr\u00fcfe die Konfiguration f\u00fcr Sensor `{entity}` und f\u00fcge geeignete Grenzen hinzu, zB `sampling_size: 20`, um das aktuelle Verhalten beizubehalten. Die Konfiguration der Statistikintegration wird mit Version 2022.12.0 flexibler und akzeptiert entweder \u201esampling_size\u201c oder \u201emax_age\u201c oder beide Einstellungen. Die obige Anfrage bereitet deine Konfiguration auf diese ansonsten bahnbrechende \u00c4nderung vor. \n\nLies die Dokumentation der Statistikintegration f\u00fcr weitere Details: https://www.home-assistant.io/integrations/statistics/", + "description": "Der Konfigurationsparameter `sampling_size` der Statistikintegration war bisher standardm\u00e4\u00dfig auf den Wert 20 eingestellt, was sich \u00e4ndern wird.\n\nBitte \u00fcberpr\u00fcfe die Konfiguration f\u00fcr Sensor `{entity}` und f\u00fcge geeignete Grenzen hinzu, z.B. `sampling_size: 20`, um das aktuelle Verhalten beizubehalten. Die Konfiguration der Statistikintegration wird mit Version 2022.12.0 flexibler und akzeptiert entweder \u201esampling_size\u201c oder \u201emax_age\u201c oder beide Einstellungen. Die obige Anfrage bereitet deine Konfiguration auf diese ansonsten bahnbrechende \u00c4nderung vor. \n\nLies die Dokumentation der Statistikintegration f\u00fcr weitere Details: https://www.home-assistant.io/integrations/statistics/", "title": "Implizite 'sampling_size' angenommen f\u00fcr eine Statistikentit\u00e4t" } } diff --git a/homeassistant/components/statistics/translations/el.json b/homeassistant/components/statistics/translations/el.json new file mode 100644 index 00000000000..f0402fc2039 --- /dev/null +++ b/homeassistant/components/statistics/translations/el.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u00abstate_characteristic\u00bb \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03b8\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 \u03c5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03ae. \n\n \u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf \u00abstate_characteristic: {characteristic} \u00bb \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u00ab {entity} \u00bb \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c4\u03b7\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03c6\u03bf\u03c1\u03ac. \n\n \u0394\u03b9\u03b1\u03b2\u03ac\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2: https://www.home-assistant.io/integrations/statistics/", + "title": "\u03a5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03cc 'state_characteristic' \u03b8\u03b5\u03c9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bc\u03b9\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03a3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ae\u03c2" + }, + "deprecation_warning_size": { + "description": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u00absampling_size\u00bb \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03ae\u03c4\u03b1\u03bd \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae 20 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae\u03c2, \u03b7 \u03bf\u03c0\u03bf\u03af\u03b1 \u03b8\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9. \n\n \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u00ab {entity} \u00bb \u03ba\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03ba\u03b1\u03c4\u03ac\u03bb\u03bb\u03b7\u03bb\u03b1 \u03cc\u03c1\u03b9\u03b1, \u03c0.\u03c7. \u00absampling_size: 20\u00bb \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c4\u03b7\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03c6\u03bf\u03c1\u03ac. \u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03b8\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 \u03c0\u03b9\u03bf \u03b5\u03c5\u03ad\u03bb\u03b9\u03ba\u03c4\u03b7 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 2022.12.0 \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \"sampling_size\" \u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \"max_age\" \u03ae \u03ba\u03b1\u03b9 \u03c4\u03b9\u03c2 \u03b4\u03cd\u03bf \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2. \u03a4\u03bf \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9 \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c4\u03bf\u03b9\u03bc\u03ac\u03b6\u03b5\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03ac \u03c4\u03b1 \u03ac\u03bb\u03bb\u03b1 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ae \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae. \n\n \u0394\u03b9\u03b1\u03b2\u03ac\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2: https://www.home-assistant.io/integrations/statistics/", + "title": "\u03a4\u03bf \u03c3\u03b9\u03c9\u03c0\u03b7\u03c1\u03cc 'sampling_size' \u03b8\u03b5\u03c9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bc\u03b9\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03a3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ae\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/es.json b/homeassistant/components/statistics/translations/es.json new file mode 100644 index 00000000000..da1b1833f57 --- /dev/null +++ b/homeassistant/components/statistics/translations/es.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "El par\u00e1metro de configuraci\u00f3n `state_characteristic` de la integraci\u00f3n de estad\u00edsticas pasar\u00e1 a ser obligatorio.\n\nPor favor, a\u00f1ade `state_characteristic: {characteristic}` a la configuraci\u00f3n del sensor `{entity}` para mantener el comportamiento actual.\n\nLee la documentaci\u00f3n de la integraci\u00f3n de estad\u00edsticas para obtener m\u00e1s detalles: https://www.home-assistant.io/integrations/statistics/", + "title": "'state_characteristic' obligatorio asumido para una entidad de estad\u00edsticas" + }, + "deprecation_warning_size": { + "description": "El par\u00e1metro de configuraci\u00f3n `sampling_size` de la integraci\u00f3n de estad\u00edsticas ten\u00eda por defecto el valor 20 hasta ahora, algo que cambiar\u00e1.\n\nPor favor, verifica la configuraci\u00f3n del sensor `{entity}` y a\u00f1ade l\u00edmites adecuados, por ejemplo, `sampling_size: 20` para mantener el comportamiento actual. La configuraci\u00f3n de la integraci\u00f3n de estad\u00edsticas ser\u00e1 m\u00e1s flexible con la versi\u00f3n 2022.12.0 y aceptar\u00e1 `sampling_size` o `max_age`, o ambas configuraciones. La solicitud anterior prepara tu configuraci\u00f3n para este cambio que, de lo contrario, ser\u00eda importante.\n\nLee la documentaci\u00f3n de la integraci\u00f3n de estad\u00edsticas para obtener m\u00e1s detalles: https://www.home-assistant.io/integrations/statistics/", + "title": "'sampling_size' impl\u00edcito asumido para una entidad de estad\u00edsticas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/et.json b/homeassistant/components/statistics/translations/et.json new file mode 100644 index 00000000000..2c7fcbe3298 --- /dev/null +++ b/homeassistant/components/statistics/translations/et.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "Statistika integreerimise konfiguratsiooniparameeter \u201estate_characteristic\u201d muutub kohustuslikuks. \n\n Praeguse k\u00e4itumise s\u00e4ilitamiseks lisa anduri {entity} konfiguratsioonile \"state_characteristic: {characteristic} \". \n\n Lisateabe saamiseks loe statistika integreerimise dokumentatsiooni: https://www.home-assistant.io/integrations/statistics/", + "title": "Statistika olemi puhul eeldatakse kohustuslikku \u201estate_characteristic\u201d." + }, + "deprecation_warning_size": { + "description": "Statistika integratsiooni konfiguratsiooniparameetri \u201esampling_size\u201d vaikev\u00e4\u00e4rtus on seni olnud 20, mis muutub. \n\n Kontrolli anduri ` {entity} ` konfiguratsiooni ja lisa praeguse k\u00e4itumise s\u00e4ilitamiseks sobivad piirid, nt `sampling_size: 20`. Statistika integreerimise konfiguratsioon muutub versiooniga 2022.12.0 paindlikumaks ja aktsepteeritakse kas \u201esampling_size\u201d v\u00f5i \u201emax_age\u201d v\u00f5i m\u00f5lemat seadet. \u00dclaltoodud taotlus valmistab teie konfiguratsiooni ette selle muidu puruneva muudatuse jaoks. \n\n Lisateabe saamiseks loe statistika integreerimise dokumentatsiooni: https://www.home-assistant.io/integrations/statistics/", + "title": "Kaudne \"sampling_size\" mida eeldatakse statistika\u00fcksuse puhul" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/he.json b/homeassistant/components/statistics/translations/he.json new file mode 100644 index 00000000000..b5cb3b24754 --- /dev/null +++ b/homeassistant/components/statistics/translations/he.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "\u05e4\u05e8\u05de\u05d8\u05e8 \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 `state_characteristic` \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05d9\u05d4\u05e4\u05d5\u05da \u05dc\u05d7\u05d5\u05d1\u05d4.\n\n\u05d9\u05e9 \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 `state_characteristic: {characteristic}` \u05dc\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d7\u05d9\u05d9\u05e9\u05df `{entity}` \u05db\u05d3\u05d9 \u05dc\u05e9\u05de\u05d5\u05e8 \u05e2\u05dc \u05d0\u05d5\u05e4\u05df \u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9.\n\n\u05d9\u05e9 \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05dc\u05e4\u05e8\u05d8\u05d9\u05dd \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd: https://www.home-assistant.io/integrations/statistics/", + "title": "\u05d7\u05d5\u05d1\u05d4 'state_characteristic' \u05e9\u05d4\u05d5\u05e0\u05d7\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4" + }, + "deprecation_warning_size": { + "description": "\u05e4\u05e8\u05de\u05d8\u05e8 \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 `sampling_size` \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc \u05dc\u05e2\u05e8\u05da 20 \u05e2\u05d3 \u05db\u05d4, \u05d0\u05e9\u05e8 \u05d9\u05e9\u05ea\u05e0\u05d4.\n\n\u05d0\u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df `{entity}` \u05d5\u05d4\u05d5\u05e1\u05e3 \u05d2\u05d1\u05d5\u05dc\u05d5\u05ea \u05de\u05ea\u05d0\u05d9\u05de\u05d9\u05dd, \u05dc\u05d3\u05d5\u05d2\u05de\u05d4, `sampling_size: 20` \u05db\u05d3\u05d9 \u05dc\u05e9\u05de\u05d5\u05e8 \u05e2\u05dc \u05d4\u05d4\u05ea\u05e0\u05d4\u05d2\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea. \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05ea\u05d4\u05e4\u05d5\u05da \u05dc\u05d2\u05de\u05d9\u05e9\u05d4 \u05d9\u05d5\u05ea\u05e8 \u05e2\u05dd \u05d2\u05e8\u05e1\u05d4 2022.12.0 \u05d5\u05ea\u05e7\u05d1\u05dc `sampling_size` \u05d0\u05d5 `max_age`, \u05d0\u05d5 \u05d0\u05ea \u05e9\u05ea\u05d9 \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d4\u05d1\u05e7\u05e9\u05d4 \u05e9\u05dc\u05e2\u05d9\u05dc \u05de\u05db\u05d9\u05e0\u05d4 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05d6\u05d4 \u05e9\u05d0\u05d7\u05e8\u05ea \u05e0\u05e9\u05d1\u05e8.\n\n\u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05dc\u05e4\u05e8\u05d8\u05d9\u05dd \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd: https://www.home-assistant.io/integrations/statistics/", + "title": "'sampling_size' \u05de\u05e8\u05d5\u05de\u05d6\u05ea \u05e9\u05d4\u05d5\u05e0\u05d7\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/hu.json b/homeassistant/components/statistics/translations/hu.json new file mode 100644 index 00000000000..df6ff936d58 --- /dev/null +++ b/homeassistant/components/statistics/translations/hu.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "A statisztikai integr\u00e1ci\u00f3 `state_characteristic` konfigur\u00e1ci\u00f3s param\u00e9tere k\u00f6telez\u0151v\u00e9 v\u00e1lik.\n\nK\u00e9rj\u00fck, a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz adja hozz\u00e1 a `state_characteristic: {characteristic}` param\u00e9tert a `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1hoz.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", + "title": "K\u00f6telez\u0151 'state_characteristic' felt\u00e9telezve egy statisztikai entit\u00e1sn\u00e1l" + }, + "deprecation_warning_size": { + "description": "A statisztikai integr\u00e1ci\u00f3 `sampling_size` konfigur\u00e1ci\u00f3s param\u00e9tere eddig 20-as \u00e9rt\u00e9kre volt alap\u00e9rtelmezve, ami v\u00e1ltozni fog.\n\nK\u00e9rj\u00fck, ellen\u0151rizze `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1t, \u00e9s adjon hozz\u00e1 megfelel\u0151 hat\u00e1rokat, pl. `sampling_size: 20` a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz. A 2022.12.0 verzi\u00f3val a statisztika integr\u00e1ci\u00f3 konfigur\u00e1ci\u00f3ja rugalmasabb\u00e1 v\u00e1lik, \u00e9s elfogadja a `sampling_size` vagy a `max_age`, vagy mindk\u00e9t be\u00e1ll\u00edt\u00e1st. Ezzel k\u00f6vetelm\u00e9nyyel felk\u00e9sz\u00edtj\u00fck a konfigur\u00e1ci\u00f3t erre a k\u00f6telez\u0151 v\u00e1ltoz\u00e1sra.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", + "title": "Implicit 'sampling_size' felt\u00e9telezve egy Statisztikai entit\u00e1shoz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/id.json b/homeassistant/components/statistics/translations/id.json new file mode 100644 index 00000000000..13ac93d21dd --- /dev/null +++ b/homeassistant/components/statistics/translations/id.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "Parameter konfigurasi `state_characteristic` dari integrasi statistik akan menjadi wajib.\n\nTambahkan `state_characteristic: {characteristic}` ke dalam konfigurasi sensor `{entity}` untuk menjaga perilaku saat ini.\n\nBaca dokumentasi integrasi Statistik untuk detail lebih lanjut: https://www.home-assistant.io/integrations/statistics/", + "title": "Parameter wajib 'state_characteristic' diasumsikan untuk entitas Statistik" + }, + "deprecation_warning_size": { + "description": "Parameter konfigurasi `sampling_size` dari integrasi Statistik yang bernilai default 20 sejauh ini, akan berubah.\n\nPeriksa konfigurasi untuk sensor `{entity}` dan tambahkan batas-batas yang sesuai, misalnya, `sampling_size: 20` untuk menjaga perilaku saat ini. Konfigurasi integrasi Statistik akan menjadi lebih fleksibel mulai versi 2022.12.0 dan menerima baik parameter `sampling_size` atau `max_age`, atau keduanya. Permintaan di atas akan mempersiapkan konfigurasi Anda untuk menghadapi perubahan besar ini.\n\nBaca dokumentasi integrasi Statistik untuk detail lebih lanjut: https://www.home-assistant.io/integrations/statistics/", + "title": "Parameter implisit 'sampling_size' diasumsikan untuk entitas Statistik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/it.json b/homeassistant/components/statistics/translations/it.json new file mode 100644 index 00000000000..f3f6ecfac53 --- /dev/null +++ b/homeassistant/components/statistics/translations/it.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "Il parametro di configurazione `state_characteristic` dell'integrazione delle statistiche diventer\u00e0 obbligatorio. \n\nSi prega di aggiungere `state_characteristic: {characteristic}` alla configurazione del sensore `{entity}` per mantenere il comportamento corrente. \n\nLeggi la documentazione dell'integrazione delle statistiche per ulteriori dettagli: https://www.home-assistant.io/integrations/statistics/", + "title": "\"state_characteristic\" obbligatoria da assumere per un'entit\u00e0 statistica" + }, + "deprecation_warning_size": { + "description": "Il parametro di configurazione `sampling_size` dell'integrazione delle statistiche \u00e8 stato impostato finora sul valore 20, il quale cambier\u00e0. \n\nSi prega di controllare la configurazione per il sensore `{entity}` e aggiungere i limiti adatti, ad esempio `sampling_size: 20`, per mantenere il comportamento corrente. La configurazione dell'integrazione delle statistiche diventer\u00e0 pi\u00f9 flessibile con la versione 2022.12.0 e accetter\u00e0 `sampling_size` o `max_age` o entrambe le impostazioni. La richiesta di cui sopra prepara la tua configurazione per questa modifica che potrebbe altrimenti causare un'interruzione. \n\nLeggi la documentazione dell'integrazione delle statistiche per ulteriori dettagli: https://www.home-assistant.io/integrations/statistics/", + "title": "\"sampling_size\" implicito assunto per un'entit\u00e0 statistica" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/no.json b/homeassistant/components/statistics/translations/no.json new file mode 100644 index 00000000000..c3e3369a59a --- /dev/null +++ b/homeassistant/components/statistics/translations/no.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "Konfigurasjonsparameteren `state_characteristic` for statistikkintegrasjonen vil bli obligatorisk. \n\n Vennligst legg til `state_characteristic: {characteristic} ` til konfigurasjonen av sensor ` {entity} ` for \u00e5 beholde gjeldende oppf\u00f8rsel. \n\n Les dokumentasjonen av statistikkintegrasjonen for ytterligere detaljer: https://www.home-assistant.io/integrations/statistics/", + "title": "Obligatorisk \"state_characteristic\" antatt for en statistikkenhet" + }, + "deprecation_warning_size": { + "description": "Konfigurasjonsparameteren `sampling_size` for statistikkintegrasjonen har standardverdien 20 s\u00e5 langt, som vil endres. \n\n Vennligst sjekk konfigurasjonen for sensor ` {entity} ` og legg til passende grenser, f.eks. `sampling_size: 20` for \u00e5 beholde gjeldende virkem\u00e5te. Konfigurasjonen av statistikkintegrasjonen vil bli mer fleksibel med versjon 2022.12.0 og godta enten `sampling_size` eller `max_age`, eller begge innstillingene. Foresp\u00f8rselen ovenfor forbereder konfigurasjonen din for denne ellers \u00f8deleggende endringen. \n\n Les dokumentasjonen av statistikkintegrasjonen for ytterligere detaljer: https://www.home-assistant.io/integrations/statistics/", + "title": "Implisitt 'sampling_size' antatt for en statistikkenhet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/ru.json b/homeassistant/components/statistics/translations/ru.json new file mode 100644 index 00000000000..0ac16d2b3e3 --- /dev/null +++ b/homeassistant/components/statistics/translations/ru.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438 `state_characteristic` \u0441\u0442\u0430\u043d\u0435\u0442 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c. \n\n\u0427\u0442\u043e\u0431\u044b \u0432\u0441\u0451 \u0440\u0430\u0431\u043e\u0442\u0430\u043b\u043e \u043a\u0430\u043a \u0438 \u043f\u0440\u0435\u0436\u0434\u0435, \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 `state_characteristic: {characteristic}` \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0435\u043d\u0441\u043e\u0440\u0430 `{entity}`. \n\n\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\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: https://www.home-assistant.io/integrations/statistics/", + "title": "\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'state_characteristic', \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438" + }, + "deprecation_warning_size": { + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438 `sampling_size` \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u043c\u0435\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 20, \u043d\u043e \u0432 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u043c \u044d\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u043e.\n\n\u0427\u0442\u043e\u0431\u044b \u0432\u0441\u0451 \u0440\u0430\u0431\u043e\u0442\u0430\u043b\u043e \u043a\u0430\u043a \u0438 \u043f\u0440\u0435\u0436\u0434\u0435, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u0430\u0442\u0447\u0438\u043a\u0430 `{entity}` \u0438 \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u043b\u0438\u043c\u0438\u0442\u044b (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `sampling_size: 20`). \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438 \u0441\u0442\u0430\u043d\u0435\u0442 \u0431\u043e\u043b\u0435\u0435 \u0433\u0438\u0431\u043a\u043e\u0439 \u0441 \u0432\u0435\u0440\u0441\u0438\u0438 2022.12.0 \u0438 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u043b\u0438\u0431\u043e `sampling_size`, \u043b\u0438\u0431\u043e `max_age`, \u043b\u0438\u0431\u043e \u043e\u0431\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430. \u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u044c\u0442\u0435 \u0412\u0430\u0448\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043a \u044d\u0442\u043e\u043c\u0443 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044e, \u0432 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043f\u043e\u043b\u043e\u043c\u043a\u0435.\n\n\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\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: https://www.home-assistant.io/integrations/statistics/.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'sampling_size', \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/tr.json b/homeassistant/components/statistics/translations/tr.json new file mode 100644 index 00000000000..8a178c28404 --- /dev/null +++ b/homeassistant/components/statistics/translations/tr.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "\u0130statistik entegrasyonunun 'state_characteristic' yap\u0131land\u0131rma parametresi zorunlu hale gelecektir. \n\n Mevcut davran\u0131\u015f\u0131 korumak i\u00e7in l\u00fctfen ` {entity} ` sens\u00f6r\u00fcn\u00fcn yap\u0131land\u0131rmas\u0131na `state_characteristic: {characteristic} ` ekleyin. \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in istatistik entegrasyonunun belgelerini okuyun: https://www.home-assistant.io/integrations/statistics/", + "title": "Bir \u0130statistik varl\u0131\u011f\u0131 i\u00e7in zorunlu 'state_characteristic' varsay\u0131ld\u0131" + }, + "deprecation_warning_size": { + "description": "\u0130statistik entegrasyonunun 'sampling_size' yap\u0131land\u0131rma parametresi \u015fu ana kadar varsay\u0131lan olarak 20 de\u011ferine ayarlanm\u0131\u015ft\u0131r ve bu de\u011fi\u015fecektir. \n\n L\u00fctfen ` {entity} ` sens\u00f6r\u00fcn\u00fcn yap\u0131land\u0131rmas\u0131n\u0131 kontrol edin ve mevcut davran\u0131\u015f\u0131 korumak i\u00e7in \u00f6rne\u011fin `sampling_size: 20` gibi uygun s\u0131n\u0131rlar ekleyin. \u0130statistik entegrasyonunun yap\u0131land\u0131rmas\u0131, 2022.12.0 s\u00fcr\u00fcm\u00fcyle daha esnek hale gelecek ve \"sampling_size\" veya \"max_age\" veya her iki ayar\u0131 da kabul edecektir. Yukar\u0131daki istek, yap\u0131land\u0131rman\u0131z\u0131 bu aksi takdirde bozulan de\u011fi\u015fiklik i\u00e7in haz\u0131rlar. \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in istatistik entegrasyonunun belgelerini okuyun: https://www.home-assistant.io/integrations/statistics/", + "title": "Bir \u0130statistik varl\u0131\u011f\u0131 i\u00e7in varsay\u0131lan \u00f6rt\u00fck 'sampling_size'" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/zh-Hant.json b/homeassistant/components/statistics/translations/zh-Hant.json new file mode 100644 index 00000000000..a396f200323 --- /dev/null +++ b/homeassistant/components/statistics/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "\u7d71\u8a08\u8cc7\u6599\u6574\u5408\u8a2d\u5b9a\u53c3\u6578 `state_characteristic`\u5c07\u8b8a\u70ba\u5f37\u5236\u8a2d\u5b9a\u3002\n\n\u8acb\u8a2d\u5b9a\u611f\u6e2c\u5668 `{entity}` \u65b0\u589e `state_characteristic: {characteristic}` \u4ee5\u4fdd\u6301\u76ee\u524d\u7684\u884c\u70ba\u3002\n\n\u8acb\u53c3\u95b1\u7d71\u8a08\u8cc7\u6599\u6574\u5408\u6587\u4ef6\u4ee5\u53d6\u5f97\u8a73\u7d30\u8cc7\u8a0a\uff1ahttps://www.home-assistant.io/integrations/statistics/", + "title": "\u7d71\u8a08\u8cc7\u6599\u5be6\u9ad4\u5f37\u5236\u96b1\u542b 'sampling_size'" + }, + "deprecation_warning_size": { + "description": "\u7d71\u8a08\u8cc7\u6599\u6574\u5408\u8a2d\u5b9a\u53c3\u6578 `sampling_size` \u9810\u8a2d\u70ba 20\u3002\u6578\u503c\u5c07\u6703\u8b8a\u66f4\u3002\n\n\u8acb\u6aa2\u67e5\u611f\u6e2c\u5668 `{entity}` \u8a2d\u5b9a\u4e26\u65b0\u589e\u9069\u7576\u7684\u7bc4\u570d\u3002\u4f8b\u5982 `sampling_size: 20` \u4ee5\u4fdd\u6301\u76ee\u524d\u7684\u884c\u70ba\u3002\u7d71\u8a08\u8cc7\u6599\u6574\u5408\u8a2d\u5b9a\u5c07\u6703\u65bc 2022.12.0 \u7248\u672c\u958b\u59cb\u8b8a\u5f97\u66f4\u70ba\u9748\u6d3b\u3001\u4e26\u63a5\u53d7 `sampling_size` \u6216 `max_age`\u3001\u6216\u8005\u5169\u500b\u8a2d\u5b9a\u3002\u4e0a\u8a34\u7684\u8981\u6c42\u8a2d\u5b9a\u5c07\u53ef\u63d0\u524d\u70ba\u65b0\u7248\u672c\u8b8a\u66f4\u505a\u597d\u6e96\u5099\u3002\n\n\u8acb\u53c3\u95b1\u7d71\u8a08\u8cc7\u6599\u6574\u5408\u6587\u4ef6\u4ee5\u53d6\u5f97\u8a73\u7d30\u8cc7\u8a0a\uff1ahttps://www.home-assistant.io/integrations/statistics/", + "title": "\u7d71\u8a08\u8cc7\u6599\u5be6\u9ad4\u5047\u5b9a\u96b1\u542b 'sampling_size'" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index f8aba1aee07..4fb91943725 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -6,5 +6,6 @@ "requirements": ["steamodd==4.21"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "loggers": ["steam"] + "loggers": ["steam"], + "integration_type": "service" } diff --git a/homeassistant/components/steam_online/translations/bg.json b/homeassistant/components/steam_online/translations/bg.json index 8d946452ca0..0f91d0affaa 100644 --- a/homeassistant/components/steam_online/translations/bg.json +++ b/homeassistant/components/steam_online/translations/bg.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_account": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d ID \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442\u0430", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/steam_online/translations/de.json b/homeassistant/components/steam_online/translations/de.json index aef08bfe269..26658f85abc 100644 --- a/homeassistant/components/steam_online/translations/de.json +++ b/homeassistant/components/steam_online/translations/de.json @@ -27,12 +27,12 @@ "issues": { "removed_yaml": { "description": "Die Konfiguration von Steam \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Steam-YAML-Konfiguration wurde entfernt" + "title": "Die Steam YAML-Konfiguration wurde entfernt" } }, "options": { "error": { - "unauthorized": "Freundesliste eingeschr\u00e4nkt: Bitte lies in der Dokumentation nach, wie du alle anderen Freunde sehen kannst" + "unauthorized": "Freundesliste eingeschr\u00e4nkt: Bitte lese in der Dokumentation nach, wie du alle anderen Freunde sehen kannst" }, "step": { "init": { diff --git a/homeassistant/components/steam_online/translations/sk.json b/homeassistant/components/steam_online/translations/sk.json new file mode 100644 index 00000000000..ef684e5b4ca --- /dev/null +++ b/homeassistant/components/steam_online/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_account": "Neplatn\u00e9 ID \u00fa\u010dtu", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "account": "ID \u00fa\u010dtu Steam", + "api_key": "API k\u013e\u00fa\u010d" + }, + "description": "Na vyh\u013eadanie ID \u00fa\u010dtu Steam pou\u017eite {account_id_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/de.json b/homeassistant/components/steamist/translations/de.json index 30aa55c998e..41522562a0c 100644 --- a/homeassistant/components/steamist/translations/de.json +++ b/homeassistant/components/steamist/translations/de.json @@ -5,7 +5,7 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", - "not_steamist_device": "Kein Steamist-Ger\u00e4t" + "not_steamist_device": "Kein Steamist Ger\u00e4t" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -14,7 +14,7 @@ "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { - "description": "M\u00f6chtest du {name} ( {ipaddress} ) einrichten?" + "description": "M\u00f6chtest du {name} ({ipaddress}) einrichten?" }, "pick_device": { "data": { diff --git a/homeassistant/components/steamist/translations/he.json b/homeassistant/components/steamist/translations/he.json index 7b8528476e1..405f691d4b3 100644 --- a/homeassistant/components/steamist/translations/he.json +++ b/homeassistant/components/steamist/translations/he.json @@ -4,7 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/steamist/translations/sk.json b/homeassistant/components/steamist/translations/sk.json index bee0999420f..65a40c229d6 100644 --- a/homeassistant/components/steamist/translations/sk.json +++ b/homeassistant/components/steamist/translations/sk.json @@ -1,7 +1,29 @@ { "config": { "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name} ({ipaddress})", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} ({ip_address})?" + }, + "pick_device": { + "data": { + "device": "Zariadenie" + } + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/sk.json b/homeassistant/components/stookalert/translations/sk.json new file mode 100644 index 00000000000..f04d4a327f4 --- /dev/null +++ b/homeassistant/components/stookalert/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 559de094090..02aad1126f8 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -20,14 +20,14 @@ import asyncio from collections.abc import Callable, Mapping import copy import logging -import re import secrets import threading import time from types import MappingProxyType -from typing import Any, Final, cast +from typing import TYPE_CHECKING, Any, Final, cast import voluptuous as vol +from yarl import URL from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -63,12 +63,16 @@ from .core import ( STREAM_SETTINGS_NON_LL_HLS, IdleTimer, KeyFrameConverter, + Orientation, StreamOutput, StreamSettings, ) from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls +if TYPE_CHECKING: + from homeassistant.components.camera import DynamicStreamSettings + __all__ = [ "ATTR_SETTINGS", "CONF_EXTRA_PART_WAIT_TIME", @@ -82,30 +86,33 @@ __all__ = [ "SOURCE_TIMEOUT", "Stream", "create_stream", + "Orientation", ] _LOGGER = logging.getLogger(__name__) -STREAM_SOURCE_REDACT_PATTERN = [ - (re.compile(r"//.*:.*@"), "//****:****@"), - (re.compile(r"\?auth=.*"), "?auth=****"), -] - -def redact_credentials(data: str) -> str: +def redact_credentials(url: str) -> str: """Redact credentials from string data.""" - for (pattern, repl) in STREAM_SOURCE_REDACT_PATTERN: - data = pattern.sub(repl, data) - return data + yurl = URL(url) + if yurl.user is not None: + yurl = yurl.with_user("****") + if yurl.password is not None: + yurl = yurl.with_password("****") + redacted_query_params = dict.fromkeys( + {"auth", "user", "password"} & yurl.query.keys(), "****" + ) + return str(yurl.update_query(redacted_query_params)) def create_stream( hass: HomeAssistant, stream_source: str, options: Mapping[str, str | bool | float], + dynamic_stream_settings: DynamicStreamSettings, stream_label: str | None = None, ) -> Stream: - """Create a stream with the specified identfier based on the source url. + """Create a stream with the specified identifier based on the source url. The stream_source is typically an rtsp url (though any url accepted by ffmpeg is fine) and options (see STREAM_OPTIONS_SCHEMA) are converted and passed into pyav / ffmpeg. @@ -154,6 +161,7 @@ def create_stream( stream_source, pyav_options=pyav_options, stream_settings=stream_settings, + dynamic_stream_settings=dynamic_stream_settings, stream_label=stream_label, ) hass.data[DOMAIN][ATTR_STREAMS].append(stream) @@ -229,7 +237,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: part_target_duration=conf[CONF_PART_DURATION], hls_advance_part_limit=max(int(3 / conf[CONF_PART_DURATION]), 3), hls_part_timeout=2 * conf[CONF_PART_DURATION], - orientation=1, ) else: hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS @@ -244,7 +251,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def shutdown(event: Event) -> None: """Stop all stream workers.""" for stream in hass.data[DOMAIN][ATTR_STREAMS]: - stream.keepalive = False + stream.dynamic_stream_settings.preload_stream = False if awaitables := [ asyncio.create_task(stream.stop()) for stream in hass.data[DOMAIN][ATTR_STREAMS] @@ -266,6 +273,7 @@ class Stream: source: str, pyav_options: dict[str, str], stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, stream_label: str | None = None, ) -> None: """Initialize a stream.""" @@ -274,14 +282,16 @@ class Stream: self.pyav_options = pyav_options self._stream_settings = stream_settings self._stream_label = stream_label - self.keepalive = False + self.dynamic_stream_settings = dynamic_stream_settings self.access_token: str | None = None self._start_stop_lock = asyncio.Lock() self._thread: threading.Thread | None = None self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False - self._keyframe_converter = KeyFrameConverter(hass, stream_settings) + self._keyframe_converter = KeyFrameConverter( + hass, stream_settings, dynamic_stream_settings + ) self._available: bool = True self._update_callback: Callable[[], None] | None = None self._logger = ( @@ -291,16 +301,6 @@ class Stream: ) self._diagnostics = Diagnostics() - @property - def orientation(self) -> int: - """Return the current orientation setting.""" - return self._stream_settings.orientation - - @orientation.setter - def orientation(self, value: int) -> None: - """Set the stream orientation setting.""" - self._stream_settings.orientation = value - def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" if fmt not in self._outputs: @@ -324,7 +324,8 @@ class Stream: async def idle_callback() -> None: if ( - not self.keepalive or fmt == RECORDER_PROVIDER + not self.dynamic_stream_settings.preload_stream + or fmt == RECORDER_PROVIDER ) and fmt in self._outputs: await self.remove_provider(self._outputs[fmt]) self.check_idle() @@ -333,6 +334,7 @@ class Stream: self.hass, IdleTimer(self.hass, timeout, idle_callback), self._stream_settings, + self.dynamic_stream_settings, ) self._outputs[fmt] = provider @@ -411,8 +413,12 @@ class Stream: while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() self.hass.add_job(self._async_update_state, True) - self._diagnostics.set_value("keepalive", self.keepalive) - self._diagnostics.set_value("orientation", self.orientation) + self._diagnostics.set_value( + "keepalive", self.dynamic_stream_settings.preload_stream + ) + self._diagnostics.set_value( + "orientation", self.dynamic_stream_settings.orientation + ) self._diagnostics.increment("start_worker") try: stream_worker( @@ -471,7 +477,7 @@ class Stream: self._outputs = {} self.access_token = None - if not self.keepalive: + if not self.dynamic_stream_settings.preload_stream: await self._stop() async def _stop(self) -> None: diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 0fa57913269..a21a9f17d96 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -5,6 +5,7 @@ import asyncio from collections import deque from collections.abc import Callable, Coroutine, Iterable import datetime +from enum import IntEnum import logging from typing import TYPE_CHECKING, Any @@ -28,6 +29,8 @@ from .const import ( if TYPE_CHECKING: from av import CodecContext, Packet + from homeassistant.components.camera import DynamicStreamSettings + from . import Stream _LOGGER = logging.getLogger(__name__) @@ -35,6 +38,19 @@ _LOGGER = logging.getLogger(__name__) PROVIDERS: Registry[str, type[StreamOutput]] = Registry() +class Orientation(IntEnum): + """Orientations for stream transforms. These are based on EXIF orientation tags.""" + + NO_TRANSFORM = 1 + MIRROR = 2 + ROTATE_180 = 3 + FLIP = 4 + ROTATE_LEFT_AND_FLIP = 5 + ROTATE_LEFT = 6 + ROTATE_RIGHT_AND_FLIP = 7 + ROTATE_RIGHT = 8 + + @attr.s(slots=True) class StreamSettings: """Stream settings.""" @@ -44,7 +60,6 @@ class StreamSettings: part_target_duration: float = attr.ib() hls_advance_part_limit: int = attr.ib() hls_part_timeout: float = attr.ib() - orientation: int = attr.ib() STREAM_SETTINGS_NON_LL_HLS = StreamSettings( @@ -53,7 +68,6 @@ STREAM_SETTINGS_NON_LL_HLS = StreamSettings( part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, hls_advance_part_limit=3, hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - orientation=1, ) @@ -259,12 +273,14 @@ class StreamOutput: hass: HomeAssistant, idle_timer: IdleTimer, stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, deque_maxlen: int | None = None, ) -> None: """Initialize a stream output.""" self._hass = hass self.idle_timer = idle_timer self.stream_settings = stream_settings + self.dynamic_stream_settings = dynamic_stream_settings self._event = asyncio.Event() self._part_event = asyncio.Event() self._segments: deque[Segment] = deque(maxlen=deque_maxlen) @@ -413,7 +429,12 @@ class KeyFrameConverter: If unsuccessful, get_image will return the previous image """ - def __init__(self, hass: HomeAssistant, stream_settings: StreamSettings) -> None: + def __init__( + self, + hass: HomeAssistant, + stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, + ) -> None: """Initialize.""" # Keep import here so that we can import stream integration without installing reqs @@ -427,6 +448,7 @@ class KeyFrameConverter: self._lock = asyncio.Lock() self._codec_context: CodecContext | None = None self._stream_settings = stream_settings + self._dynamic_stream_settings = dynamic_stream_settings def create_codec_context(self, codec_context: CodecContext) -> None: """ @@ -484,12 +506,13 @@ class KeyFrameConverter: if frames: frame = frames[0] if width and height: - if self._stream_settings.orientation >= 5: + if self._dynamic_stream_settings.orientation >= 5: frame = frame.reformat(width=height, height=width) else: frame = frame.reformat(width=width, height=height) bgr_array = self.transform_image( - frame.to_ndarray(format="bgr24"), self._stream_settings.orientation + frame.to_ndarray(format="bgr24"), + self._dynamic_stream_settings.orientation, ) self._image = bytes(self._turbojpeg.encode(bgr_array)) diff --git a/homeassistant/components/stream/diagnostics.py b/homeassistant/components/stream/diagnostics.py index 47370eeb5f9..13fd70cc957 100644 --- a/homeassistant/components/stream/diagnostics.py +++ b/homeassistant/components/stream/diagnostics.py @@ -19,7 +19,7 @@ class Diagnostics: self._values: dict[str, Any] = {} def increment(self, key: str) -> None: - """Increment a counter for the spcified key/event.""" + """Increment a counter for the specified key/event.""" self._counter.update(Counter({key: 1})) def set_value(self, key: str, value: Any) -> None: diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 35d32d5b0e3..5ec27a1768c 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -6,6 +6,8 @@ from typing import TYPE_CHECKING from homeassistant.exceptions import HomeAssistantError +from .core import Orientation + if TYPE_CHECKING: from io import BufferedIOBase @@ -179,22 +181,24 @@ ROTATE_LEFT_FLIP = (ZERO32 + NEGONE32 + ZERO32) + (NEGONE32 + ZERO32 + ZERO32) ROTATE_RIGHT_FLIP = (ZERO32 + ONE32 + ZERO32) + (ONE32 + ZERO32 + ZERO32) TRANSFORM_MATRIX_TOP = ( - # The first two entries are just to align the indices with the EXIF orientation tags - b"", - b"", - MIRROR, - ROTATE_180, - FLIP, - ROTATE_LEFT_FLIP, - ROTATE_LEFT, - ROTATE_RIGHT_FLIP, - ROTATE_RIGHT, + # The index into this tuple corresponds to the EXIF orientation tag + # Only index values of 2 through 8 are used + # The first two entries are just to keep everything aligned + b"", # 0 + b"", # 1 + MIRROR, # 2 + ROTATE_180, # 3 + FLIP, # 4 + ROTATE_LEFT_FLIP, # 5 + ROTATE_LEFT, # 6 + ROTATE_RIGHT_FLIP, # 7 + ROTATE_RIGHT, # 8 ) -def transform_init(init: bytes, orientation: int) -> bytes: +def transform_init(init: bytes, orientation: Orientation) -> bytes: """Change the transformation matrix in the header.""" - if orientation == 1: + if orientation == Orientation.NO_TRANSFORM: return init # Find moov moov_location = next(find_box(init, b"moov")) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index e8920abcaa6..cddb4413ed8 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -27,6 +27,8 @@ from .core import ( from .fmp4utils import get_codec_string, transform_init if TYPE_CHECKING: + from homeassistant.components.camera import DynamicStreamSettings + from . import Stream @@ -50,9 +52,16 @@ class HlsStreamOutput(StreamOutput): hass: HomeAssistant, idle_timer: IdleTimer, stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, ) -> None: """Initialize HLS output.""" - super().__init__(hass, idle_timer, stream_settings, deque_maxlen=MAX_SEGMENTS) + super().__init__( + hass, + idle_timer, + stream_settings, + dynamic_stream_settings, + deque_maxlen=MAX_SEGMENTS, + ) self._target_duration = stream_settings.min_segment_duration @property @@ -339,7 +348,7 @@ class HlsInitView(StreamView): if not (segments := track.get_segments()) or not (body := segments[0].init): return web.HTTPNotFound() return web.Response( - body=transform_init(body, stream.orientation), + body=transform_init(body, stream.dynamic_stream_settings.orientation), headers={"Content-Type": "video/mp4"}, ) diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 1f79da20542..815acd5b39c 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0"], + "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0", "numpy==1.23.2"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index e917292251a..fffbd489757 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -21,6 +21,8 @@ from .fmp4utils import read_init, transform_init if TYPE_CHECKING: import deque + from homeassistant.components.camera import DynamicStreamSettings + _LOGGER = logging.getLogger(__name__) @@ -38,9 +40,10 @@ class RecorderOutput(StreamOutput): hass: HomeAssistant, idle_timer: IdleTimer, stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, ) -> None: """Initialize recorder output.""" - super().__init__(hass, idle_timer, stream_settings) + super().__init__(hass, idle_timer, stream_settings, dynamic_stream_settings) self.video_path: str @property @@ -154,7 +157,7 @@ class RecorderOutput(StreamOutput): video_path, mode="wb" ) as out_file: init = transform_init( - read_init(in_file), self.stream_settings.orientation + read_init(in_file), self.dynamic_stream_settings.orientation ) out_file.write(init) in_file.seek(len(init)) diff --git a/homeassistant/components/subaru/diagnostics.py b/homeassistant/components/subaru/diagnostics.py new file mode 100644 index 00000000000..79ffcbe1792 --- /dev/null +++ b/homeassistant/components/subaru/diagnostics.py @@ -0,0 +1,56 @@ +"""Diagnostics for the Subaru integration.""" +from __future__ import annotations + +from typing import Any + +from subarulink.const import LATITUDE, LONGITUDE, ODOMETER, VEHICLE_NAME + +from homeassistant.components.diagnostics.util import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN, ENTRY_COORDINATOR, VEHICLE_VIN + +CONFIG_FIELDS_TO_REDACT = [CONF_USERNAME, CONF_PASSWORD, CONF_PIN, CONF_DEVICE_ID] +DATA_FIELDS_TO_REDACT = [VEHICLE_VIN, VEHICLE_NAME, LATITUDE, LONGITUDE, ODOMETER] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] + + diagnostics_data = { + "config_entry": async_redact_data(config_entry.data, CONFIG_FIELDS_TO_REDACT), + "options": async_redact_data(config_entry.options, []), + "data": [ + async_redact_data(info, DATA_FIELDS_TO_REDACT) + for info in coordinator.data.values() + ], + } + + return diagnostics_data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] + + vin = next(iter(device.identifiers))[1] + + if info := coordinator.data.get(vin): + return { + "config_entry": async_redact_data( + config_entry.data, CONFIG_FIELDS_TO_REDACT + ), + "options": async_redact_data(config_entry.options, []), + "data": async_redact_data(info, DATA_FIELDS_TO_REDACT), + } + + raise HomeAssistantError("Device not found") diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index df3a97cbda3..d0e1193f92b 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -3,7 +3,7 @@ "name": "Subaru", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/subaru", - "requirements": ["subarulink==0.6.1"], + "requirements": ["subarulink==0.7.0"], "codeowners": ["@G-Two"], "iot_class": "cloud_polling", "loggers": ["stdiomask", "subarulink"] diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index cae5a7b14a4..9db2d329210 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -14,12 +14,10 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ELECTRIC_POTENTIAL_VOLT, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, PRESSURE_HPA, - TEMP_CELSIUS, VOLUME_GALLONS, VOLUME_LITERS, ) @@ -117,20 +115,6 @@ API_GEN_2_SENSORS = [ native_unit_of_measurement=PRESSURE_HPA, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( - key=sc.EXTERNAL_TEMP, - device_class=SensorDeviceClass.TEMPERATURE, - name="External temp", - native_unit_of_measurement=TEMP_CELSIUS, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=sc.BATTERY_VOLTAGE, - device_class=SensorDeviceClass.VOLTAGE, - name="12V battery voltage", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=SensorStateClass.MEASUREMENT, - ), ] # Sensors available to "Subaru Safety Plus" subscribers with PHEV vehicles diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index c43cb84d5f6..00813f34380 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -10,6 +10,9 @@ "incorrect_validation_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434 \u0437\u0430 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435" }, "step": { + "two_factor": { + "description": "\u0418\u0437\u0438\u0441\u043a\u0432\u0430 \u0441\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "country": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u044a\u0440\u0436\u0430\u0432\u0430", diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index 2156df0d1a8..bfb5ad316be 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -11,7 +11,7 @@ "incorrect_pin": "Falsche PIN", "incorrect_validation_code": "Falscher Validierungscode", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "two_factor_request_failed": "Anfrage f\u00fcr 2FA-Code fehlgeschlagen, bitte versuche es erneut" + "two_factor_request_failed": "Anfrage f\u00fcr 2FA Code fehlgeschlagen, bitte versuche es erneut" }, "step": { "pin": { diff --git a/homeassistant/components/subaru/translations/sk.json b/homeassistant/components/subaru/translations/sk.json index 5ada995aa6e..d3a5c8afd3a 100644 --- a/homeassistant/components/subaru/translations/sk.json +++ b/homeassistant/components/subaru/translations/sk.json @@ -1,7 +1,39 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "error": { + "bad_pin_format": "PIN by mal ma\u0165 4 \u010d\u00edslice", + "bad_validation_code_format": "Overovac\u00ed k\u00f3d by mal ma\u0165 6 \u010d\u00edslic", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "incorrect_pin": "Nespr\u00e1vny PIN", + "incorrect_validation_code": "Nespr\u00e1vny overovac\u00ed k\u00f3d", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + } + }, + "two_factor": { + "description": "Vy\u017eaduje sa dvojfaktorov\u00e9 overenie" + }, + "two_factor_validate": { + "data": { + "validation_code": "Overovac\u00ed k\u00f3d" + }, + "description": "Zadajte prijat\u00fd overovac\u00ed k\u00f3d" + }, + "user": { + "data": { + "country": "Vyberte krajinu", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sun/translations/sk.json b/homeassistant/components/sun/translations/sk.json index a5bc4d5339e..e7cabacaf10 100644 --- a/homeassistant/components/sun/translations/sk.json +++ b/homeassistant/components/sun/translations/sk.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + }, "state": { "_": { "above_horizon": "Nad horizontom", diff --git a/homeassistant/components/surepetcare/translations/sk.json b/homeassistant/components/surepetcare/translations/sk.json index 1b1e671c054..5d12e6d8fe7 100644 --- a/homeassistant/components/surepetcare/translations/sk.json +++ b/homeassistant/components/surepetcare/translations/sk.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "password": "Heslo" + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } } } diff --git a/homeassistant/components/switch/translations/is.json b/homeassistant/components/switch/translations/is.json index 35751f3f4f6..2f6c55fb1b6 100644 --- a/homeassistant/components/switch/translations/is.json +++ b/homeassistant/components/switch/translations/is.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} er sl\u00f6kkt", + "is_on": "{entity_name} er kveikt" + } + }, "state": { "_": { "off": "Sl\u00f6kkt", diff --git a/homeassistant/components/switch/translations/sk.json b/homeassistant/components/switch/translations/sk.json index 3f871c8a4a1..297ea3159d0 100644 --- a/homeassistant/components/switch/translations/sk.json +++ b/homeassistant/components/switch/translations/sk.json @@ -1,4 +1,15 @@ { + "device_automation": { + "action_type": { + "turn_off": "Vypn\u00fa\u0165 {entity_name}", + "turn_on": "Zapn\u00fa\u0165 {entity_name}" + }, + "trigger_type": { + "changed_states": "{entity_name} zapnut\u00e9 alebo vypnut\u00e9", + "turned_off": "{entity_name} vypnut\u00e1", + "turned_on": "{entity_name} zapnut\u00e1" + } + }, "state": { "_": { "off": "Vypnut\u00fd", diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 991f4f33a6b..8b6527eb49e 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaConfigFlowHandler, SchemaFlowFormStep, - SchemaFlowMenuStep, wrapped_entity_config_entry_title, ) @@ -25,7 +24,7 @@ TARGET_DOMAIN_OPTIONS = [ selector.SelectOptionDict(value=Platform.SIREN, label="Siren"), ] -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { +CONFIG_FLOW = { "user": SchemaFlowFormStep( vol.Schema( { diff --git a/homeassistant/components/switch_as_x/translations/bg.json b/homeassistant/components/switch_as_x/translations/bg.json index f5c2a6e0433..e7491f78010 100644 --- a/homeassistant/components/switch_as_x/translations/bg.json +++ b/homeassistant/components/switch_as_x/translations/bg.json @@ -7,5 +7,6 @@ } } } - } + }, + "title": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u0442\u0438\u043f\u0430 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0430 \u043f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0432\u0430\u0442\u0435\u043b" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/sk.json b/homeassistant/components/switch_as_x/translations/sk.json new file mode 100644 index 00000000000..64f2700a3fb --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_id": "Prep\u00edna\u010d", + "target_domain": "Nov\u00fd typ" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json index 9ff8e548255..5f3ceb35165 100644 --- a/homeassistant/components/switch_as_x/translations/sv.json +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -6,9 +6,9 @@ "entity_id": "Brytare", "target_domain": "Typ" }, - "description": "V\u00e4lj en str\u00f6mbrytare som du vill visa i Home Assistant som lampa, skal eller n\u00e5got annat. Den ursprungliga switchen kommer att d\u00f6ljas." + "description": "V\u00e4lj en brytare som du vill visa i Home Assistant som lampa, gardin eller n\u00e5got annat. Den ursprungliga brytaren kommer att d\u00f6ljas." } } }, - "title": "\u00c4ndra enhetstyp f\u00f6r en c" + "title": "\u00c4ndra enhetstyp f\u00f6r en brytare" } \ No newline at end of file diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json index 75e5b2e9bfd..f7bcfef85a4 100644 --- a/homeassistant/components/switchbee/manifest.json +++ b/homeassistant/components/switchbee/manifest.json @@ -3,7 +3,7 @@ "name": "SwitchBee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/switchbee", - "requirements": ["pyswitchbee==1.5.5"], + "requirements": ["pyswitchbee==1.6.1"], "codeowners": ["@jafar-atili"], "iot_class": "local_polling" } diff --git a/homeassistant/components/switchbee/translations/ca.json b/homeassistant/components/switchbee/translations/ca.json index 745b3ed79cf..204f9ab801d 100644 --- a/homeassistant/components/switchbee/translations/ca.json +++ b/homeassistant/components/switchbee/translations/ca.json @@ -13,20 +13,10 @@ "data": { "host": "Amfitri\u00f3", "password": "Contrasenya", - "switch_as_light": "Inicialitza els interruptors com a entitats de llum", "username": "Nom d'usuari" }, "description": "Configura la integraci\u00f3 SwitchBee amb Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Dispositius a incloure" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/cs.json b/homeassistant/components/switchbee/translations/cs.json index 4b40956d82b..0f02cd974c2 100644 --- a/homeassistant/components/switchbee/translations/cs.json +++ b/homeassistant/components/switchbee/translations/cs.json @@ -17,14 +17,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Zahrnout za\u0159\u00edzen\u00ed" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/de.json b/homeassistant/components/switchbee/translations/de.json index 99862c61d84..a943e05be6f 100644 --- a/homeassistant/components/switchbee/translations/de.json +++ b/homeassistant/components/switchbee/translations/de.json @@ -13,19 +13,9 @@ "data": { "host": "Host", "password": "Passwort", - "switch_as_light": "Schalter als Lichteinheiten initialisieren", "username": "Benutzername" }, - "description": "Einrichten der SwitchBee-Integration mit Home Assistant." - } - } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Einzuschlie\u00dfende Ger\u00e4te" - } + "description": "Einrichten der SwitchBee Integration mit Home Assistant." } } } diff --git a/homeassistant/components/switchbee/translations/el.json b/homeassistant/components/switchbee/translations/el.json index ad806294956..e0e460c7823 100644 --- a/homeassistant/components/switchbee/translations/el.json +++ b/homeassistant/components/switchbee/translations/el.json @@ -13,20 +13,10 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "switch_as_light": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03c4\u03ce\u03bd \u03c9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 SwitchBee \u03bc\u03b5 \u03c4\u03bf Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/en.json b/homeassistant/components/switchbee/translations/en.json index 41f9ee6a043..555d0e03f34 100644 --- a/homeassistant/components/switchbee/translations/en.json +++ b/homeassistant/components/switchbee/translations/en.json @@ -13,20 +13,10 @@ "data": { "host": "Host", "password": "Password", - "switch_as_light": "Initialize switches as light entities", "username": "Username" }, "description": "Setup SwitchBee integration with Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Devices to include" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/es.json b/homeassistant/components/switchbee/translations/es.json index fc22055048d..8df68cfeaa3 100644 --- a/homeassistant/components/switchbee/translations/es.json +++ b/homeassistant/components/switchbee/translations/es.json @@ -13,20 +13,10 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "switch_as_light": "Inicializar interruptores como entidades luces", "username": "Nombre de usuario" }, "description": "Configurar la integraci\u00f3n SwitchBee con Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Dispositivos a incluir" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/et.json b/homeassistant/components/switchbee/translations/et.json index fa6c114d434..d43a291194e 100644 --- a/homeassistant/components/switchbee/translations/et.json +++ b/homeassistant/components/switchbee/translations/et.json @@ -13,20 +13,10 @@ "data": { "host": "Host", "password": "Salas\u00f5na", - "switch_as_light": "L\u00e4htesta l\u00fclitid valgusolemitena", "username": "Kasutajanimi" }, "description": "Seadista SwitchBee sidumine Home Assistantiga." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Kaasatavad seadmed" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/fr.json b/homeassistant/components/switchbee/translations/fr.json index 1f1d36f5da0..eb09f124182 100644 --- a/homeassistant/components/switchbee/translations/fr.json +++ b/homeassistant/components/switchbee/translations/fr.json @@ -13,20 +13,10 @@ "data": { "host": "H\u00f4te", "password": "Mot de passe", - "switch_as_light": "Initialiser les commutateurs en tant que lumi\u00e8res", "username": "Nom d'utilisateur" }, "description": "Configurez l'int\u00e9gration de SwitchBee avec Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Appareils \u00e0 inclure" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/hu.json b/homeassistant/components/switchbee/translations/hu.json index 01b3bc0010b..debc3d0956f 100644 --- a/homeassistant/components/switchbee/translations/hu.json +++ b/homeassistant/components/switchbee/translations/hu.json @@ -13,20 +13,10 @@ "data": { "host": "C\u00edm", "password": "Jelsz\u00f3", - "switch_as_light": "A kapcsol\u00f3k l\u00e1mpak\u00e9nt t\u00f6rt\u00e9n\u0151 inicializ\u00e1l\u00e1sa", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "SwitchBee integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa a Home Assistant rendszerrel." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "A benne foglalt eszk\u00f6z\u00f6k" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/id.json b/homeassistant/components/switchbee/translations/id.json index 71fc2092c6c..3476e712583 100644 --- a/homeassistant/components/switchbee/translations/id.json +++ b/homeassistant/components/switchbee/translations/id.json @@ -13,20 +13,10 @@ "data": { "host": "Host", "password": "Kata Sandi", - "switch_as_light": "Inisialisasi sakelar sebagai entitas lampu", "username": "Nama Pengguna" }, "description": "Siapkan integrasi SwitchBee dengan Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Perangkat yang disertakan" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/it.json b/homeassistant/components/switchbee/translations/it.json index ebd7915f049..4694ab92b85 100644 --- a/homeassistant/components/switchbee/translations/it.json +++ b/homeassistant/components/switchbee/translations/it.json @@ -13,20 +13,10 @@ "data": { "host": "Host", "password": "Password", - "switch_as_light": "Inizializza gli interruttori come entit\u00e0 luce", "username": "Nome utente" }, "description": "Imposta l'integrazione di SwitchBee con Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Dispositivi da includere" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/ja.json b/homeassistant/components/switchbee/translations/ja.json index b0bdd860094..a9d2ddfd3ac 100644 --- a/homeassistant/components/switchbee/translations/ja.json +++ b/homeassistant/components/switchbee/translations/ja.json @@ -17,14 +17,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "\u542b\u3081\u308b\u30c7\u30d0\u30a4\u30b9" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/ko.json b/homeassistant/components/switchbee/translations/ko.json index 5af63d34313..d9b021bccbc 100644 --- a/homeassistant/components/switchbee/translations/ko.json +++ b/homeassistant/components/switchbee/translations/ko.json @@ -3,19 +3,9 @@ "step": { "user": { "data": { - "switch_as_light": "\uc2a4\uc704\uce58\ub97c \uc870\uba85 \uac1c\uccb4\ub85c \ucd08\uae30\ud654", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "\ud3ec\ud568\ud560 \uc7a5\uce58" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/no.json b/homeassistant/components/switchbee/translations/no.json index 22f16d5a328..d97d103cd1d 100644 --- a/homeassistant/components/switchbee/translations/no.json +++ b/homeassistant/components/switchbee/translations/no.json @@ -13,20 +13,10 @@ "data": { "host": "Vert", "password": "Passord", - "switch_as_light": "Initialiser brytere som lysenheter", "username": "Brukernavn" }, "description": "Sett opp SwitchBee-integrasjon med Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Enheter som skal inkluderes" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/pl.json b/homeassistant/components/switchbee/translations/pl.json index f2233b1550c..2859ba20d73 100644 --- a/homeassistant/components/switchbee/translations/pl.json +++ b/homeassistant/components/switchbee/translations/pl.json @@ -13,20 +13,10 @@ "data": { "host": "Nazwa hosta lub adres IP", "password": "Has\u0142o", - "switch_as_light": "Zainicjuj prze\u0142\u0105czniki jako encje \u015bwiat\u0142a", "username": "Nazwa u\u017cytkownika" }, "description": "Konfiguracja integracji SwitchBee z Home Assistantem." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Urz\u0105dzenia do uwzgl\u0119dnienia" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/pt-BR.json b/homeassistant/components/switchbee/translations/pt-BR.json index a99ffe41150..098b62a109e 100644 --- a/homeassistant/components/switchbee/translations/pt-BR.json +++ b/homeassistant/components/switchbee/translations/pt-BR.json @@ -13,20 +13,10 @@ "data": { "host": "Nome do host", "password": "Senha", - "switch_as_light": "Inicializar switches como entidades de luz", "username": "Nome de usu\u00e1rio" }, "description": "Configure a integra\u00e7\u00e3o do SwitchBee com o Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Dispositivos para incluir" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/ru.json b/homeassistant/components/switchbee/translations/ru.json index 13ea7317296..a4900df81c4 100644 --- a/homeassistant/components/switchbee/translations/ru.json +++ b/homeassistant/components/switchbee/translations/ru.json @@ -13,20 +13,10 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "switch_as_light": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0435\u0439 \u043a\u0430\u043a \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 SwitchBee." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/sk.json b/homeassistant/components/switchbee/translations/sk.json new file mode 100644 index 00000000000..666f6e28840 --- /dev/null +++ b/homeassistant/components/switchbee/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/sv.json b/homeassistant/components/switchbee/translations/sv.json index 42d3330f48e..b99d85f4a86 100644 --- a/homeassistant/components/switchbee/translations/sv.json +++ b/homeassistant/components/switchbee/translations/sv.json @@ -13,20 +13,10 @@ "data": { "host": "V\u00e4rd", "password": "L\u00f6senord", - "switch_as_light": "Initiera omkopplare som ljusenheter", "username": "Anv\u00e4ndarnamn" }, "description": "Konfigurera SwitchBee-integrationen med Home Assistant." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Enheter att inkludera" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/tr.json b/homeassistant/components/switchbee/translations/tr.json index b3bd4cde1b1..bb708bf8b1c 100644 --- a/homeassistant/components/switchbee/translations/tr.json +++ b/homeassistant/components/switchbee/translations/tr.json @@ -13,20 +13,10 @@ "data": { "host": "Sunucu", "password": "Parola", - "switch_as_light": "Anahtarlar\u0131 \u0131\u015f\u0131k varl\u0131klar\u0131 olarak ba\u015flat", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Home Assistant ile SwitchBee entegrasyonunu kurun." } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "Dahil edilecek cihazlar" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/zh-Hant.json b/homeassistant/components/switchbee/translations/zh-Hant.json index baa704a5235..94dfc1a466c 100644 --- a/homeassistant/components/switchbee/translations/zh-Hant.json +++ b/homeassistant/components/switchbee/translations/zh-Hant.json @@ -13,20 +13,10 @@ "data": { "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc", - "switch_as_light": "\u5c07\u958b\u95dc\u521d\u59cb\u70ba\u71c8\u5149\u5be6\u9ad4", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8a2d\u5b9a SwitchBee \u63a5\u5165 Home Assistant \u6574\u5408\u3002" } } - }, - "options": { - "step": { - "init": { - "data": { - "devices": "\u5305\u542b\u88dd\u7f6e" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 274c5784b2f..831d14d8459 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.5"], + "requirements": ["PySwitchbot==0.22.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/translations/bg.json b/homeassistant/components/switchbot/translations/bg.json index 196b5511f77..218cc017cc8 100644 --- a/homeassistant/components/switchbot/translations/bg.json +++ b/homeassistant/components/switchbot/translations/bg.json @@ -19,10 +19,7 @@ }, "user": { "data": { - "address": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", - "mac": "MAC \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", - "name": "\u0418\u043c\u0435", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + "address": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" } } } diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 04c1af6d69f..b117dd02c05 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Adre\u00e7a del dispositiu", - "mac": "Adre\u00e7a MAC del dispositiu", - "name": "Nom", - "password": "Contrasenya" - }, - "title": "Configuraci\u00f3 de dispositiu Switchbot" + "address": "Adre\u00e7a del dispositiu" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Nombre de reintents", - "retry_timeout": "Temps d'espera entre reintents", - "scan_timeout": "Quant de temps s'ha d'escanejar en busca de dades d'alerta", - "update_time": "Temps entre actualitzacions (segons)" + "retry_count": "Nombre de reintents" } } } diff --git a/homeassistant/components/switchbot/translations/cs.json b/homeassistant/components/switchbot/translations/cs.json index 5e57c3b60a7..0c72857af0c 100644 --- a/homeassistant/components/switchbot/translations/cs.json +++ b/homeassistant/components/switchbot/translations/cs.json @@ -14,12 +14,6 @@ "data": { "password": "Heslo" } - }, - "user": { - "data": { - "name": "Jm\u00e9no", - "password": "Heslo" - } } } } diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 2b4fd5ba8cf..fa62c6fa343 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Ger\u00e4teadresse", - "mac": "MAC-Adresse des Ger\u00e4ts", - "name": "Name", - "password": "Passwort" - }, - "title": "Switchbot-Ger\u00e4t einrichten" + "address": "Ger\u00e4teadresse" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Anzahl der Wiederholungen", - "retry_timeout": "Zeit\u00fcberschreitung zwischen Wiederholungsversuchen", - "scan_timeout": "Wie lange nach Anzeigendaten suchen", - "update_time": "Zeit zwischen Aktualisierungen (Sekunden)" + "retry_count": "Anzahl der Wiederholungen" } } } diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index df151167751..096a6d05fe9 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -24,12 +24,8 @@ }, "user": { "data": { - "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", - "mac": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Switchbot" + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + } } } }, @@ -37,10 +33,7 @@ "step": { "init": { "data": { - "retry_count": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03ce\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03b9\u03ce\u03bd", - "retry_timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ae\u03c8\u03b5\u03c9\u03bd", - "scan_timeout": "\u03a0\u03cc\u03c3\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03c6\u03ae\u03bc\u03b9\u03c3\u03b7\u03c2", - "update_time": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" + "retry_count": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03ce\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03b9\u03ce\u03bd" } } } diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 6a0231c9bed..e262ed53175 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Device address", - "mac": "Device MAC address", - "name": "Name", - "password": "Password" - }, - "title": "Setup Switchbot device" + "address": "Device address" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data", - "update_time": "Time between updates (seconds)" + "retry_count": "Retry count" } } } diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index a4cca573f7d..55a374de412 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Direcci\u00f3n del dispositivo", - "mac": "Direcci\u00f3n MAC del dispositivo", - "name": "Nombre", - "password": "Contrase\u00f1a" - }, - "title": "Configurar el dispositivo Switchbot" + "address": "Direcci\u00f3n del dispositivo" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Recuento de reintentos", - "retry_timeout": "Tiempo de espera entre reintentos", - "scan_timeout": "Cu\u00e1nto tiempo escanear en busca de datos de anuncio", - "update_time": "Tiempo entre actualizaciones (segundos)" + "retry_count": "Recuento de reintentos" } } } diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 0f5998dee99..ad76e84f976 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Seadme aadress", - "mac": "Seadme MAC-aadress", - "name": "Nimi", - "password": "Salas\u00f5na" - }, - "title": "Switchbot seadme seadistamine" + "address": "Seadme aadress" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Korduskatsete arv", - "retry_timeout": "Korduskatsete vaheline aeg", - "scan_timeout": "Kui kaua andmeid otsida", - "update_time": "V\u00e4rskenduste vaheline aeg (sekundites)" + "retry_count": "Korduskatsete arv" } } } diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index 2b35ec9634e..cf8a3709a3d 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Adresse de l'appareil", - "mac": "Adresse MAC de l'appareil", - "name": "Nom", - "password": "Mot de passe" - }, - "title": "Configurer l'appareil Switchbot" + "address": "Adresse de l'appareil" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Nombre de nouvelles tentatives", - "retry_timeout": "D\u00e9lai d'attente entre les tentatives", - "scan_timeout": "Dur\u00e9e de la recherche de donn\u00e9es publicitaires", - "update_time": "Intervalle de temps entre deux mises \u00e0 jour (en secondes)" + "retry_count": "Nombre de nouvelles tentatives" } } } diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index b4cb968ff23..223bce5b5c7 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -11,13 +11,6 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } - }, - "user": { - "data": { - "name": "\u05e9\u05dd", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "title": "\u05d4\u05ea\u05e7\u05e0\u05ea \u05d1\u05d5\u05e8\u05e8 \u05db\u05d9\u05d5\u05d5\u05e0\u05d5\u05df" } } }, @@ -25,10 +18,7 @@ "step": { "init": { "data": { - "retry_count": "\u05e1\u05e4\u05d9\u05e8\u05ea \u05e0\u05e1\u05d9\u05d5\u05e0\u05d5\u05ea \u05d7\u05d5\u05d6\u05e8\u05d9\u05dd", - "retry_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05d1\u05d9\u05df \u05e0\u05d9\u05e1\u05d9\u05d5\u05e0\u05d5\u05ea \u05d7\u05d5\u05d6\u05e8\u05d9\u05dd", - "scan_timeout": "\u05db\u05de\u05d4 \u05d6\u05de\u05df \u05dc\u05e1\u05e8\u05d5\u05e7 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05e4\u05e8\u05e1\u05d5\u05de\u05ea", - "update_time": "\u05d6\u05de\u05df \u05d1\u05d9\u05df \u05e2\u05d3\u05db\u05d5\u05e0\u05d9\u05dd (\u05e9\u05e0\u05d9\u05d5\u05ea)" + "retry_count": "\u05e1\u05e4\u05d9\u05e8\u05ea \u05e0\u05e1\u05d9\u05d5\u05e0\u05d5\u05ea \u05d7\u05d5\u05d6\u05e8\u05d9\u05dd" } } } diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 88f5756c5fa..52f020dcf1e 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -24,12 +24,8 @@ }, "user": { "data": { - "address": "Eszk\u00f6z c\u00edme", - "mac": "Eszk\u00f6z MAC-c\u00edme", - "name": "Elnevez\u00e9s", - "password": "Jelsz\u00f3" - }, - "title": "Switchbot eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" + "address": "Eszk\u00f6z c\u00edme" + } } } }, @@ -37,10 +33,7 @@ "step": { "init": { "data": { - "retry_count": "\u00dajrapr\u00f3b\u00e1lkoz\u00e1sok sz\u00e1ma", - "retry_timeout": "\u00dajrapr\u00f3b\u00e1lkoz\u00e1sok k\u00f6z\u00f6tti id\u0151korl\u00e1t", - "scan_timeout": "Mennyi ideig keresse a hirdet\u00e9si adatokat", - "update_time": "Friss\u00edt\u00e9sek k\u00f6z\u00f6tti id\u0151 (m\u00e1sodperc)" + "retry_count": "\u00dajrapr\u00f3b\u00e1lkoz\u00e1sok sz\u00e1ma" } } } diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index d9d4ade13b1..c7f4cbcc466 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Alamat perangkat", - "mac": "Alamat MAC perangkat", - "name": "Nama", - "password": "Kata Sandi" - }, - "title": "Siapkan perangkat Switchbot" + "address": "Alamat perangkat" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Jumlah percobaan", - "retry_timeout": "Tenggang waktu antara percobaan ulang", - "scan_timeout": "Berapa lama untuk memindai data iklan", - "update_time": "Waktu antara pembaruan (detik)" + "retry_count": "Jumlah percobaan" } } } diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index 3592ce065e3..515c4bdd173 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -24,12 +24,8 @@ }, "user": { "data": { - "address": "Indirizzo del dispositivo", - "mac": "Indirizzo MAC del dispositivo", - "name": "Nome", - "password": "Password" - }, - "title": "Imposta il dispositivo Switchbot" + "address": "Indirizzo del dispositivo" + } } } }, @@ -37,10 +33,7 @@ "step": { "init": { "data": { - "retry_count": "Conteggio dei tentativi di ripetizione", - "retry_timeout": "Tempo scaduto tra i tentativi", - "scan_timeout": "Per quanto tempo eseguire la scansione dei dati pubblicitari", - "update_time": "Tempo tra gli aggiornamenti (secondi)" + "retry_count": "Conteggio dei tentativi di ripetizione" } } } diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 133c9f44d86..b51fde14699 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -23,12 +23,8 @@ }, "user": { "data": { - "address": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30c9\u30ec\u30b9", - "mac": "\u30c7\u30d0\u30a4\u30b9\u306eMAC\u30a2\u30c9\u30ec\u30b9", - "name": "\u540d\u524d", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "title": "Switchbot\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "address": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30c9\u30ec\u30b9" + } } } }, @@ -36,10 +32,7 @@ "step": { "init": { "data": { - "retry_count": "\u518d\u8a66\u884c\u56de\u6570", - "retry_timeout": "\u518d\u8a66\u884c\u306e\u9593\u306e\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", - "scan_timeout": "\u5e83\u544a\u30c7\u30fc\u30bf\u3092\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u6642\u9593", - "update_time": "\u66f4\u65b0\u9593\u9694(\u79d2)" + "retry_count": "\u518d\u8a66\u884c\u56de\u6570" } } } diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index 1860024e011..66720f89013 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -16,14 +16,6 @@ "data": { "password": "Wachtwoord" } - }, - "user": { - "data": { - "mac": "MAC-adres apparaat", - "name": "Naam", - "password": "Wachtwoord" - }, - "title": "Switchbot-apparaat instellen" } } }, @@ -31,10 +23,7 @@ "step": { "init": { "data": { - "retry_count": "Aantal herhalingen", - "retry_timeout": "Time-out tussen nieuwe pogingen", - "scan_timeout": "Hoe lang te scannen voor advertentiegegevens", - "update_time": "Tijd tussen updates (seconden)" + "retry_count": "Aantal herhalingen" } } } diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 53767627d2d..bb3ca766c70 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Enhetsadresse", - "mac": "Enhetens MAC -adresse", - "name": "Navn", - "password": "Passord" - }, - "title": "Sett opp Switchbot-enhet" + "address": "Enhetsadresse" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Antall nye fors\u00f8k", - "retry_timeout": "Tidsavbrudd mellom fors\u00f8k", - "scan_timeout": "Hvor lenge skal jeg s\u00f8ke etter annonsedata", - "update_time": "Tid mellom oppdateringer (sekunder)" + "retry_count": "Antall nye fors\u00f8k" } } } diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 5dbc87c07af..ffe36408692 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -26,12 +26,8 @@ }, "user": { "data": { - "address": "Adres urz\u0105dzenia", - "mac": "Adres MAC urz\u0105dzenia", - "name": "Nazwa", - "password": "Has\u0142o" - }, - "title": "Konfiguracja urz\u0105dzenia Switchbot" + "address": "Adres urz\u0105dzenia" + } } } }, @@ -39,10 +35,7 @@ "step": { "init": { "data": { - "retry_count": "Liczba ponownych pr\u00f3b", - "retry_timeout": "Limit czasu mi\u0119dzy kolejnymi pr\u00f3bami", - "scan_timeout": "Jak d\u0142ugo skanowa\u0107 w poszukiwaniu danych reklamowych", - "update_time": "Czas mi\u0119dzy aktualizacjami (sekundy)" + "retry_count": "Liczba ponownych pr\u00f3b" } } } diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 8508185870c..6fe4662469b 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -24,12 +24,8 @@ }, "user": { "data": { - "address": "Endere\u00e7o do dispositivo", - "mac": "Endere\u00e7o MAC do dispositivo", - "name": "Nome", - "password": "Senha" - }, - "title": "Configurar dispositivo Switchbot" + "address": "Endere\u00e7o do dispositivo" + } } } }, @@ -37,10 +33,7 @@ "step": { "init": { "data": { - "retry_count": "Contagem de tentativas", - "retry_timeout": "Intervalo entre tentativas", - "scan_timeout": "Quanto tempo para verificar os dados do an\u00fancio", - "update_time": "Tempo entre atualiza\u00e7\u00f5es (segundos)" + "retry_count": "Contagem de tentativas" } } } diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index ddf26f9a40e..4bd32239c72 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Switchbot" + "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u044b\u0445 \u043f\u043e\u043f\u044b\u0442\u043e\u043a", - "retry_timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u044b\u043c\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0430\u043c\u0438", - "scan_timeout": "\u041a\u0430\u043a \u0434\u043e\u043b\u0433\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "update_time": "\u0412\u0440\u0435\u043c\u044f \u043c\u0435\u0436\u0434\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043c\u0438 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + "retry_count": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u044b\u0445 \u043f\u043e\u043f\u044b\u0442\u043e\u043a" } } } diff --git a/homeassistant/components/switchbot/translations/sk.json b/homeassistant/components/switchbot/translations/sk.json index af15f92c2f2..20403a167ff 100644 --- a/homeassistant/components/switchbot/translations/sk.json +++ b/homeassistant/components/switchbot/translations/sk.json @@ -1,9 +1,25 @@ { "config": { + "abort": { + "already_configured_device": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_unconfigured_devices": "Nena\u0161li sa \u017eiadne nenakonfigurovan\u00e9 zariadenia.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "password": { + "data": { + "password": "Heslo" + }, + "description": "Zariadenie {name} vy\u017eaduje heslo" + }, "user": { "data": { - "name": "N\u00e1zov" + "address": "Adresa zariadenia" } } } diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index 8124e80fd73..7caefebe073 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "Enhetsadress", - "mac": "Enhetens MAC-adress", - "name": "Namn", - "password": "L\u00f6senord" - }, - "title": "Konfigurera Switchbot-enhet" + "address": "Enhetsadress" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "Antal ompr\u00f6vningar", - "retry_timeout": "Timeout mellan \u00e5terf\u00f6rs\u00f6k", - "scan_timeout": "Hur l\u00e4nge ska man s\u00f6ka efter annonsdata", - "update_time": "Tid mellan uppdateringar (sekunder)" + "retry_count": "Antal ompr\u00f6vningar" } } } diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index d60be4c0d73..f135a79b549 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -24,12 +24,8 @@ }, "user": { "data": { - "address": "Cihaz adresi", - "mac": "Cihaz MAC adresi", - "name": "Ad", - "password": "Parola" - }, - "title": "Switchbot cihaz\u0131n\u0131 kurun" + "address": "Cihaz adresi" + } } } }, @@ -37,10 +33,7 @@ "step": { "init": { "data": { - "retry_count": "Yeniden deneme say\u0131s\u0131", - "retry_timeout": "Yeniden denemeler aras\u0131ndaki zaman a\u015f\u0131m\u0131", - "scan_timeout": "Reklam verilerinin taranmas\u0131 ne kadar s\u00fcrer?", - "update_time": "G\u00fcncellemeler aras\u0131ndaki s\u00fcre (saniye)" + "retry_count": "Yeniden deneme say\u0131s\u0131" } } } diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 082ad32f84c..43611eeb571 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -20,12 +20,8 @@ }, "user": { "data": { - "address": "\u88dd\u7f6e\u4f4d\u5740", - "mac": "\u88dd\u7f6e MAC \u4f4d\u5740", - "name": "\u540d\u7a31", - "password": "\u5bc6\u78bc" - }, - "title": "\u8a2d\u5b9a Switchbot \u88dd\u7f6e" + "address": "\u88dd\u7f6e\u4f4d\u5740" + } } } }, @@ -33,10 +29,7 @@ "step": { "init": { "data": { - "retry_count": "\u91cd\u8a66\u6b21\u6578", - "retry_timeout": "\u903e\u6642", - "scan_timeout": "\u6383\u63cf\u5ee3\u544a\u6578\u64da\u7684\u6642\u9593", - "update_time": "\u66f4\u65b0\u9593\u9694\u6642\u9593\uff08\u79d2\uff09" + "retry_count": "\u91cd\u8a66\u6b21\u6578" } } } diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index be8f140711a..39710be4857 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -29,7 +29,13 @@ from .const import ( ) from .utils import async_start_bridge, async_stop_bridge -PLATFORMS = [Platform.CLIMATE, Platform.COVER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BUTTON, + Platform.CLIMATE, + Platform.COVER, + Platform.SENSOR, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py new file mode 100644 index 00000000000..756acc1366e --- /dev/null +++ b/homeassistant/components/switcher_kis/button.py @@ -0,0 +1,158 @@ +"""Switcher integration Button platform.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from dataclasses import dataclass + +from aioswitcher.api import ( + DeviceState, + SwitcherBaseResponse, + SwitcherType2Api, + ThermostatSwing, +) +from aioswitcher.api.remotes import SwitcherBreezeRemote +from aioswitcher.device import DeviceCategory + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import SwitcherDataUpdateCoordinator +from .const import SIGNAL_DEVICE_ADD +from .utils import get_breeze_remote_manager + + +@dataclass +class SwitcherThermostatButtonDescriptionMixin: + """Mixin to describe a Switcher Thermostat Button entity.""" + + press_fn: Callable[[SwitcherType2Api, SwitcherBreezeRemote], SwitcherBaseResponse] + supported: Callable[[SwitcherBreezeRemote], bool] + + +@dataclass +class SwitcherThermostatButtonEntityDescription( + ButtonEntityDescription, SwitcherThermostatButtonDescriptionMixin +): + """Class to describe a Switcher Thermostat Button entity.""" + + +THERMOSTAT_BUTTONS = [ + SwitcherThermostatButtonEntityDescription( + key="assume_on", + name="Assume on", + icon="mdi:fan", + entity_category=EntityCategory.CONFIG, + press_fn=lambda api, remote: api.control_breeze_device( + remote, state=DeviceState.ON, update_state=True + ), + supported=lambda remote: bool(remote.on_off_type), + ), + SwitcherThermostatButtonEntityDescription( + key="assume_off", + name="Assume off", + icon="mdi:fan-off", + entity_category=EntityCategory.CONFIG, + press_fn=lambda api, remote: api.control_breeze_device( + remote, state=DeviceState.OFF, update_state=True + ), + supported=lambda remote: bool(remote.on_off_type), + ), + SwitcherThermostatButtonEntityDescription( + key="vertical_swing_on", + name="Vertical swing on", + icon="mdi:autorenew", + press_fn=lambda api, remote: api.control_breeze_device( + remote, swing=ThermostatSwing.ON + ), + supported=lambda remote: bool(remote.separated_swing_command), + ), + SwitcherThermostatButtonEntityDescription( + key="vertical_swing_off", + name="Vertical swing off", + icon="mdi:autorenew-off", + press_fn=lambda api, remote: api.control_breeze_device( + remote, swing=ThermostatSwing.OFF + ), + supported=lambda remote: bool(remote.separated_swing_command), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Switcher button from config entry.""" + + async def async_add_buttons(coordinator: SwitcherDataUpdateCoordinator) -> None: + """Get remote and add button from Switcher device.""" + if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT: + remote: SwitcherBreezeRemote = await hass.async_add_executor_job( + get_breeze_remote_manager(hass).get_remote, coordinator.data.remote_id + ) + async_add_entities( + SwitcherThermostatButtonEntity(coordinator, description, remote) + for description in THERMOSTAT_BUTTONS + if description.supported(remote) + ) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_buttons) + ) + + +class SwitcherThermostatButtonEntity( + CoordinatorEntity[SwitcherDataUpdateCoordinator], ButtonEntity +): + """Representation of a Switcher climate entity.""" + + entity_description: SwitcherThermostatButtonEntityDescription + + def __init__( + self, + coordinator: SwitcherDataUpdateCoordinator, + description: SwitcherThermostatButtonEntityDescription, + remote: SwitcherBreezeRemote, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.entity_description = description + self._remote = remote + + self._attr_name = f"{coordinator.name} {description.name}" + self._attr_unique_id = f"{coordinator.mac_address}-{description.key}" + self._attr_device_info = DeviceInfo( + connections={ + (device_registry.CONNECTION_NETWORK_MAC, coordinator.mac_address) + } + ) + + async def async_press(self) -> None: + """Press the button.""" + response: SwitcherBaseResponse = None + error = None + + try: + async with SwitcherType2Api( + self.coordinator.data.ip_address, self.coordinator.data.device_id + ) as swapi: + response = await self.entity_description.press_fn(swapi, self._remote) + except (asyncio.TimeoutError, OSError, RuntimeError) as err: + error = repr(err) + + if error or not response or not response.successful: + self.coordinator.last_update_success = False + self.async_write_ha_state() + raise HomeAssistantError( + f"Call api for {self.name} failed, " + f"response/error: {response or error}" + ) diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index 99b9208e4ad..01f4c80da31 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -5,7 +5,7 @@ import asyncio from typing import Any, cast from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api -from aioswitcher.api.remotes import SwitcherBreezeRemote, SwitcherBreezeRemoteManager +from aioswitcher.api.remotes import SwitcherBreezeRemote from aioswitcher.device import ( DeviceCategory, DeviceState, @@ -37,6 +37,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SwitcherDataUpdateCoordinator from .const import SIGNAL_DEVICE_ADD +from .utils import get_breeze_remote_manager DEVICE_MODE_TO_HA = { ThermostatMode.COOL: HVACMode.COOL, @@ -64,13 +65,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Switcher climate from config entry.""" - remote_manager = SwitcherBreezeRemoteManager() async def async_add_climate(coordinator: SwitcherDataUpdateCoordinator) -> None: """Get remote and add climate from Switcher device.""" if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT: remote: SwitcherBreezeRemote = await hass.async_add_executor_job( - remote_manager.get_remote, coordinator.data.remote_id + get_breeze_remote_manager(hass).get_remote, coordinator.data.remote_id ) async_add_entities([SwitcherClimateEntity(coordinator, remote)]) @@ -104,7 +104,6 @@ class SwitcherClimateEntity( self._attr_target_temperature_step = 1 self._attr_temperature_unit = TEMP_CELSIUS - self._attr_supported_features = 0 self._attr_hvac_modes = [HVACMode.OFF] for mode in remote.modes_features: self._attr_hvac_modes.append(DEVICE_MODE_TO_HA[mode]) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 14f324d8cac..9206b08c197 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,7 +3,7 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi", "@thecode"], - "requirements": ["aioswitcher==3.1.0"], + "requirements": ["aioswitcher==3.2.1"], "quality_scale": "platinum", "iot_class": "local_push", "config_flow": true, diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index c8dced8663c..34a4de3e9d3 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -30,8 +30,8 @@ class AttributeDescription: name: str icon: str | None = None unit: str | None = None - device_class: str | None = None - state_class: str | None = None + device_class: SensorDeviceClass | None = None + state_class: SensorStateClass | None = None default_enabled: bool = True diff --git a/homeassistant/components/switcher_kis/translations/he.json b/homeassistant/components/switcher_kis/translations/he.json index d3d68dccc93..4eafc6dc29b 100644 --- a/homeassistant/components/switcher_kis/translations/he.json +++ b/homeassistant/components/switcher_kis/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/switcher_kis/translations/sk.json b/homeassistant/components/switcher_kis/translations/sk.json new file mode 100644 index 00000000000..d4bb209c34c --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/utils.py b/homeassistant/components/switcher_kis/utils.py index 5a35be8aa95..ad0414ae806 100644 --- a/homeassistant/components/switcher_kis/utils.py +++ b/homeassistant/components/switcher_kis/utils.py @@ -6,9 +6,11 @@ from collections.abc import Callable import logging from typing import Any +from aioswitcher.api.remotes import SwitcherBreezeRemoteManager from aioswitcher.bridge import SwitcherBase, SwitcherBridge from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import singleton from .const import DATA_BRIDGE, DISCOVERY_TIME_SEC, DOMAIN @@ -53,3 +55,9 @@ async def async_discover_devices() -> dict[str, SwitcherBase]: _LOGGER.debug("Finished discovery, discovered devices: %s", len(discovered_devices)) return discovered_devices + + +@singleton.singleton("switcher_breeze_remote_manager") +def get_breeze_remote_manager(hass: HomeAssistant) -> SwitcherBreezeRemoteManager: + """Get Switcher Breeze remote manager.""" + return SwitcherBreezeRemoteManager() diff --git a/homeassistant/components/syncthing/translations/sk.json b/homeassistant/components/syncthing/translations/sk.json index 5ada995aa6e..18c5c12cd5e 100644 --- a/homeassistant/components/syncthing/translations/sk.json +++ b/homeassistant/components/syncthing/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/sk.json b/homeassistant/components/syncthru/translations/sk.json index 3d28cc36f74..5b20b432a32 100644 --- a/homeassistant/components/syncthru/translations/sk.json +++ b/homeassistant/components/syncthru/translations/sk.json @@ -1,14 +1,25 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "invalid_url": "Neplatn\u00e1 adresa URL", + "syncthru_not_supported": "Zariadenie nepodporuje SyncThru", + "unknown_state": "Nezn\u00e1my stav tla\u010diarne, overte URL a sie\u0165ov\u00e9 pripojenie" + }, + "flow_title": "{name}", "step": { "confirm": { "data": { - "name": "N\u00e1zov" + "name": "N\u00e1zov", + "url": "URL webov\u00e9ho rozhrania" } }, "user": { "data": { - "name": "N\u00e1zov" + "name": "N\u00e1zov", + "url": "URL webov\u00e9ho rozhrania" } } } diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 0314165eb41..ba332ca7e7d 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -164,6 +164,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): use_ssl = user_input.get(CONF_SSL, DEFAULT_USE_SSL) verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) otp_code = user_input.get(CONF_OTP_CODE) + friendly_name = user_input.get(CONF_NAME) if not port: if use_ssl is True: @@ -229,7 +230,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reconfigure_successful") - return self.async_create_entry(title=host, data=config_data) + return self.async_create_entry(title=friendly_name or host, data=config_data) async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -303,6 +304,8 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self.reauth_conf = entry_data + self.context["title_placeholders"][CONF_HOST] = entry_data[CONF_HOST] + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index dcd0a5ab730..038e32254e1 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "reconfigure_successful": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "otp_failed": "\u0414\u0432\u0443\u0441\u0442\u0435\u043f\u0435\u043d\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u0441 \u043d\u043e\u0432 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 247eba408c9..e4b9041441d 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -54,7 +54,7 @@ "init": { "data": { "scan_interval": "Minuten zwischen den Scans", - "snap_profile_type": "Qualit\u00e4tsstufe der Kamera-Schnappsch\u00fcsse (0:hoch 1:mittel 2:niedrig)", + "snap_profile_type": "Qualit\u00e4tsstufe der Kamera-Schnappsch\u00fcsse (0: hoch, 1: mittel, 2: niedrig)", "timeout": "Timeout (Sekunden)" } } diff --git a/homeassistant/components/synology_dsm/translations/sk.json b/homeassistant/components/synology_dsm/translations/sk.json index 83965998f0e..a530e78e9b8 100644 --- a/homeassistant/components/synology_dsm/translations/sk.json +++ b/homeassistant/components/synology_dsm/translations/sk.json @@ -5,17 +5,51 @@ "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "flow_title": "{name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "K\u00f3d" + } + }, "link": { "data": { - "port": "Port" - } + "password": "Heslo", + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + }, + "description": "Chcete nastavi\u0165 {name} ({host})?" + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Synology DSM Znova overi\u0165 integr\u00e1ciu" }, "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Min\u00faty medzi skenovaniami" } } } diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 9370de70787..f386ed57085 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -9,6 +9,7 @@ "dependencies": ["media_source"], "after_dependencies": ["zeroconf"], "quality_scale": "silver", + "integration_type": "device", "iot_class": "local_push", "loggers": ["systembridgeconnector"] } diff --git a/homeassistant/components/system_bridge/translations/bg.json b/homeassistant/components/system_bridge/translations/bg.json index 25a1b280f57..42b204c86cc 100644 --- a/homeassistant/components/system_bridge/translations/bg.json +++ b/homeassistant/components/system_bridge/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/system_bridge/translations/sk.json b/homeassistant/components/system_bridge/translations/sk.json index 4a6f823bbe6..97b3c8c2677 100644 --- a/homeassistant/components/system_bridge/translations/sk.json +++ b/homeassistant/components/system_bridge/translations/sk.json @@ -1,12 +1,16 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "flow_title": "{name}", "step": { "authenticate": { "data": { @@ -16,8 +20,10 @@ "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", + "host": "Hostite\u013e", "port": "Port" - } + }, + "description": "Zadajte podrobnosti o pripojen\u00ed." } } } diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index a2db68f11c7..a231c3e83f3 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.9.3"], + "requirements": ["psutil==5.9.4"], "codeowners": [], "iot_class": "local_push", "loggers": ["psutil"] diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index eb889264151..d16e2ac3190 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -97,13 +97,13 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { "ipv4_address": SysMonitorSensorEntityDescription( key="ipv4_address", name="IPv4 address", - icon="mdi:server-network", + icon="mdi:ip-network", mandatory_arg=True, ), "ipv6_address": SysMonitorSensorEntityDescription( key="ipv6_address", name="IPv6 address", - icon="mdi:server-network", + icon="mdi:ip-network", mandatory_arg=True, ), "last_boot": SysMonitorSensorEntityDescription( diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 5f58203e9e8..0e8b968ca7c 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -245,7 +245,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._supported_hvac_modes = supported_hvac_modes self._supported_fan_modes = supported_fan_modes - self._support_flags = support_flags + self._attr_supported_features = support_flags self._available = False @@ -286,11 +286,6 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): ) ) - @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - @property def name(self): """Return the name of the entity.""" @@ -466,7 +461,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def swing_modes(self): """Swing modes for the device.""" - if self._support_flags & ClimateEntityFeature.SWING_MODE: + if self.supported_features & ClimateEntityFeature.SWING_MODE: return [TADO_SWING_ON, TADO_SWING_OFF] return None @@ -618,10 +613,10 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): temperature_to_send = None fan_speed = None - if self._support_flags & ClimateEntityFeature.FAN_MODE: + if self.supported_features & ClimateEntityFeature.FAN_MODE: fan_speed = self._current_tado_fan_speed swing = None - if self._support_flags & ClimateEntityFeature.SWING_MODE: + if self.supported_features & ClimateEntityFeature.SWING_MODE: swing = self._current_tado_swing_mode self._tado.set_zone_overlay( diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 078c821eeb4..529b4bcfb97 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": ["python-tado==0.12.0"], - "codeowners": ["@michaelarnauts", "@north3221"], + "codeowners": ["@michaelarnauts"], "config_flow": true, "homekit": { "models": ["tado", "AC02"] diff --git a/homeassistant/components/tado/translations/sk.json b/homeassistant/components/tado/translations/sk.json index 5ada995aa6e..1ed85afb928 100644 --- a/homeassistant/components/tado/translations/sk.json +++ b/homeassistant/components/tado/translations/sk.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo" + }, + "title": "Pripojte sa k svojmu \u00fa\u010dtu Tado" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index c05b4416343..090835103f9 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -108,7 +108,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @bind_hass async def async_scan_tag( - hass: HomeAssistant, tag_id: str, device_id: str, context: Context | None = None + hass: HomeAssistant, + tag_id: str, + device_id: str | None, + context: Context | None = None, ) -> None: """Handle when a tag is scanned.""" if DOMAIN not in hass.config.components: diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index 146521dfba9..b6d77737eab 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -1,4 +1,6 @@ """Support for tag triggers.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.const import CONF_PLATFORM @@ -26,8 +28,10 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Listen for tag_scanned events based on configuration.""" trigger_data = trigger_info["trigger_data"] - tag_ids = set(config[TAG_ID]) - device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None + tag_ids: set[str] = set(config[TAG_ID]) + device_ids: set[str] | None = ( + set(config[DEVICE_ID]) if DEVICE_ID in config else None + ) job = HassJob(action) diff --git a/homeassistant/components/tailscale/translations/bg.json b/homeassistant/components/tailscale/translations/bg.json index a580355fe12..8ec410d2f18 100644 --- a/homeassistant/components/tailscale/translations/bg.json +++ b/homeassistant/components/tailscale/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/tailscale/translations/sk.json b/homeassistant/components/tailscale/translations/sk.json index 4eba3bdc8bb..b09983030d2 100644 --- a/homeassistant/components/tailscale/translations/sk.json +++ b/homeassistant/components/tailscale/translations/sk.json @@ -4,6 +4,7 @@ "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { diff --git a/homeassistant/components/tankerkoenig/translations/bg.json b/homeassistant/components/tankerkoenig/translations/bg.json index 8631e4a1daa..f2e73b70b6d 100644 --- a/homeassistant/components/tankerkoenig/translations/bg.json +++ b/homeassistant/components/tankerkoenig/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" @@ -22,14 +22,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/ca.json b/homeassistant/components/tankerkoenig/translations/ca.json index 46d523276e7..aa0e032b369 100644 --- a/homeassistant/components/tankerkoenig/translations/ca.json +++ b/homeassistant/components/tankerkoenig/translations/ca.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Interval d'actualitzaci\u00f3", "show_on_map": "Mostra les estacions al mapa", "stations": "Estacions" }, diff --git a/homeassistant/components/tankerkoenig/translations/cs.json b/homeassistant/components/tankerkoenig/translations/cs.json index 3bfe94e68dd..8c13cb40b74 100644 --- a/homeassistant/components/tankerkoenig/translations/cs.json +++ b/homeassistant/components/tankerkoenig/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/tankerkoenig/translations/de.json b/homeassistant/components/tankerkoenig/translations/de.json index f0ad25857a5..2a23f855e57 100644 --- a/homeassistant/components/tankerkoenig/translations/de.json +++ b/homeassistant/components/tankerkoenig/translations/de.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Update-Intervall", "show_on_map": "Stationen auf der Karte anzeigen", "stations": "Stationen" }, diff --git a/homeassistant/components/tankerkoenig/translations/el.json b/homeassistant/components/tankerkoenig/translations/el.json index 82dc5b9019b..1c11770bf9c 100644 --- a/homeassistant/components/tankerkoenig/translations/el.json +++ b/homeassistant/components/tankerkoenig/translations/el.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2", "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03ce\u03bd \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7", "stations": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af" }, diff --git a/homeassistant/components/tankerkoenig/translations/en.json b/homeassistant/components/tankerkoenig/translations/en.json index 64c585f838b..9a69c8c812e 100644 --- a/homeassistant/components/tankerkoenig/translations/en.json +++ b/homeassistant/components/tankerkoenig/translations/en.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Update Interval", "show_on_map": "Show stations on map", "stations": "Stations" }, diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index c83d6d68c74..66af638e9a3 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Intervalo de actualizaci\u00f3n", "show_on_map": "Muestra las estaciones en el mapa", "stations": "Estaciones" }, diff --git a/homeassistant/components/tankerkoenig/translations/et.json b/homeassistant/components/tankerkoenig/translations/et.json index 028bac46d44..0270d26833e 100644 --- a/homeassistant/components/tankerkoenig/translations/et.json +++ b/homeassistant/components/tankerkoenig/translations/et.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "V\u00e4rskendamise intervall", "show_on_map": "N\u00e4ita jaamu kaardil", "stations": "Tanklad" }, diff --git a/homeassistant/components/tankerkoenig/translations/fr.json b/homeassistant/components/tankerkoenig/translations/fr.json index 410150263ab..c683f3113b9 100644 --- a/homeassistant/components/tankerkoenig/translations/fr.json +++ b/homeassistant/components/tankerkoenig/translations/fr.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Intervalle de mise \u00e0 jour", "show_on_map": "Afficher les stations-services sur la carte", "stations": "Stations-services" }, diff --git a/homeassistant/components/tankerkoenig/translations/hu.json b/homeassistant/components/tankerkoenig/translations/hu.json index 502ccd6fd9c..4910226697c 100644 --- a/homeassistant/components/tankerkoenig/translations/hu.json +++ b/homeassistant/components/tankerkoenig/translations/hu.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Friss\u00edt\u00e9si id\u0151k\u00f6z", "show_on_map": "\u00c1llom\u00e1sok megjelen\u00edt\u00e9se a t\u00e9rk\u00e9pen", "stations": "\u00c1llom\u00e1sok" }, diff --git a/homeassistant/components/tankerkoenig/translations/id.json b/homeassistant/components/tankerkoenig/translations/id.json index ed0e2e15104..0ada5b4f42f 100644 --- a/homeassistant/components/tankerkoenig/translations/id.json +++ b/homeassistant/components/tankerkoenig/translations/id.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Interval pembaruan", "show_on_map": "Tampilkan SPBU di peta", "stations": "SPBU" }, diff --git a/homeassistant/components/tankerkoenig/translations/it.json b/homeassistant/components/tankerkoenig/translations/it.json index b98598d10a3..99c937d7e6b 100644 --- a/homeassistant/components/tankerkoenig/translations/it.json +++ b/homeassistant/components/tankerkoenig/translations/it.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Intervallo di aggiornamento", "show_on_map": "Mostra stazioni sulla mappa", "stations": "Stazioni" }, diff --git a/homeassistant/components/tankerkoenig/translations/ja.json b/homeassistant/components/tankerkoenig/translations/ja.json index 45e3233f2fd..7e3f4dad642 100644 --- a/homeassistant/components/tankerkoenig/translations/ja.json +++ b/homeassistant/components/tankerkoenig/translations/ja.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "\u66f4\u65b0\u9593\u9694", "show_on_map": "\u5730\u56f3\u4e0a\u306b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8868\u793a\u3059\u308b", "stations": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" }, diff --git a/homeassistant/components/tankerkoenig/translations/nl.json b/homeassistant/components/tankerkoenig/translations/nl.json index 57a8a1fcfe7..f2061ee4f30 100644 --- a/homeassistant/components/tankerkoenig/translations/nl.json +++ b/homeassistant/components/tankerkoenig/translations/nl.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Update Interval", "show_on_map": "Toon stations op kaart", "stations": "Stations" }, diff --git a/homeassistant/components/tankerkoenig/translations/no.json b/homeassistant/components/tankerkoenig/translations/no.json index f0eac9a8f0e..a4683753d8e 100644 --- a/homeassistant/components/tankerkoenig/translations/no.json +++ b/homeassistant/components/tankerkoenig/translations/no.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Oppdateringsintervall", "show_on_map": "Vis stasjoner p\u00e5 kart", "stations": "Stasjoner" }, diff --git a/homeassistant/components/tankerkoenig/translations/pl.json b/homeassistant/components/tankerkoenig/translations/pl.json index 288b4f8aae7..2f49c8dbd3a 100644 --- a/homeassistant/components/tankerkoenig/translations/pl.json +++ b/homeassistant/components/tankerkoenig/translations/pl.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji", "show_on_map": "Poka\u017c stacje na mapie", "stations": "Stacje" }, diff --git a/homeassistant/components/tankerkoenig/translations/pt-BR.json b/homeassistant/components/tankerkoenig/translations/pt-BR.json index af26b6167b3..af87f41252c 100644 --- a/homeassistant/components/tankerkoenig/translations/pt-BR.json +++ b/homeassistant/components/tankerkoenig/translations/pt-BR.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Intervalo de atualiza\u00e7\u00e3o", "show_on_map": "Mostrar postos no mapa", "stations": "Esta\u00e7\u00f5es" }, diff --git a/homeassistant/components/tankerkoenig/translations/ru.json b/homeassistant/components/tankerkoenig/translations/ru.json index d2b34eb264b..b444e5d3d17 100644 --- a/homeassistant/components/tankerkoenig/translations/ru.json +++ b/homeassistant/components/tankerkoenig/translations/ru.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" }, diff --git a/homeassistant/components/tankerkoenig/translations/sk.json b/homeassistant/components/tankerkoenig/translations/sk.json index 06c74b52725..0be1d37c18b 100644 --- a/homeassistant/components/tankerkoenig/translations/sk.json +++ b/homeassistant/components/tankerkoenig/translations/sk.json @@ -1,8 +1,27 @@ { "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "select_station": { + "data": { + "stations": "Stanice" + } + }, "user": { "data": { + "api_key": "API k\u013e\u00fa\u010d", + "location": "Umiestnenie", "radius": "Polomer vyh\u013ead\u00e1vania" } } diff --git a/homeassistant/components/tankerkoenig/translations/sv.json b/homeassistant/components/tankerkoenig/translations/sv.json index 55c362cc717..a2f8d953e28 100644 --- a/homeassistant/components/tankerkoenig/translations/sv.json +++ b/homeassistant/components/tankerkoenig/translations/sv.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Uppdateringsintervall", "show_on_map": "Visa stationer p\u00e5 kartan", "stations": "Stationer" }, diff --git a/homeassistant/components/tankerkoenig/translations/tr.json b/homeassistant/components/tankerkoenig/translations/tr.json index ca0038b6dbb..c75a35c8a79 100644 --- a/homeassistant/components/tankerkoenig/translations/tr.json +++ b/homeassistant/components/tankerkoenig/translations/tr.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "G\u00fcncelle\u015ftirme aral\u0131\u011f\u0131", "show_on_map": "\u0130stasyonlar\u0131 haritada g\u00f6ster", "stations": "\u0130stasyonlar" }, diff --git a/homeassistant/components/tankerkoenig/translations/zh-Hant.json b/homeassistant/components/tankerkoenig/translations/zh-Hant.json index 1e1a5d6e15a..cfc12640c3b 100644 --- a/homeassistant/components/tankerkoenig/translations/zh-Hant.json +++ b/homeassistant/components/tankerkoenig/translations/zh-Hant.json @@ -37,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "\u66f4\u65b0\u983b\u7387", "show_on_map": "\u65bc\u5730\u5716\u986f\u793a\u52a0\u6cb9\u7ad9", "stations": "\u52a0\u6cb9\u7ad9" }, diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index da9d7e0d53b..2123ee74f1b 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -102,7 +102,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for platform in PLATFORMS: hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format(platform))() - # deattach device triggers + # detach device triggers device_registry = dr.async_get(hass) devices = async_entries_for_config_entry(device_registry, entry.entry_id) for device in devices: diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 2ff51752ea5..b7b695c260f 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -120,7 +120,7 @@ class TasmotaLight( def _setup_from_entity(self) -> None: """(Re)Setup the entity.""" self._supported_color_modes = set() - supported_features = 0 + supported_features = LightEntityFeature(0) light_type = self._tasmota_entity.light_type if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: diff --git a/homeassistant/components/tasmota/translations/cs.json b/homeassistant/components/tasmota/translations/cs.json index 673126f1cf0..5abe099df04 100644 --- a/homeassistant/components/tasmota/translations/cs.json +++ b/homeassistant/components/tasmota/translations/cs.json @@ -8,5 +8,10 @@ "description": "Chcete nastavit Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "N\u011bkolik za\u0159\u00edzen\u00ed Tasmota sd\u00edl\u00ed t\u00e9ma {topic} . \n\n Za\u0159\u00edzen\u00ed Tasmota s t\u00edmto probl\u00e9mem: {offenders} ." + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json index cd1da24e22c..b8a2cf9a2a7 100644 --- a/homeassistant/components/tasmota/translations/de.json +++ b/homeassistant/components/tasmota/translations/de.json @@ -19,12 +19,12 @@ }, "issues": { "topic_duplicated": { - "description": "Mehrere Tasmota-Ger\u00e4te teilen das Topic {topic} . \n\nTasmota-Ger\u00e4te mit diesem Problem: {offenders} .", - "title": "Mehrere Tasmota-Ger\u00e4te teilen das gleiche Topic" + "description": "Mehrere Tasmota Ger\u00e4te teilen das Topic {topic}. \n\nTasmota Ger\u00e4te mit diesem Problem: {offenders}.", + "title": "Mehrere Tasmota Ger\u00e4te teilen das gleiche Topic" }, "topic_no_prefix": { - "description": "Tasmota-Ger\u00e4t {name} mit IP {ip} enth\u00e4lt `%prefix%` nicht in seinem vollst\u00e4ndigen Topic. \n\nEntit\u00e4ten f\u00fcr diese Ger\u00e4te sind deaktiviert, bis die Konfiguration korrigiert wurde.", - "title": "Tasmota-Ger\u00e4t {name} hat ein ung\u00fcltiges MQTT-Topic" + "description": "Tasmota Ger\u00e4t {name} mit IP {ip} enth\u00e4lt `%prefix%` nicht in seinem vollst\u00e4ndigen Topic. \n\nEntit\u00e4ten f\u00fcr diese Ger\u00e4te sind deaktiviert, bis die Konfiguration korrigiert wurde.", + "title": "Tasmota Ger\u00e4t {name} hat ein ung\u00fcltiges MQTT-Topic" } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/fr.json b/homeassistant/components/tasmota/translations/fr.json index 9ead135ce72..d24dbdf53b9 100644 --- a/homeassistant/components/tasmota/translations/fr.json +++ b/homeassistant/components/tasmota/translations/fr.json @@ -19,6 +19,7 @@ }, "issues": { "topic_duplicated": { + "description": "Plusieurs appareils Tasmota partagent le sujet {topic}.\n\nAppareils Tasmota avec ce probl\u00e8me : {offenders}.", "title": "Plusieurs appareils Tasmota partagent le m\u00eame sujet" }, "topic_no_prefix": { diff --git a/homeassistant/components/tasmota/translations/he.json b/homeassistant/components/tasmota/translations/he.json index 2bc04cca267..bfded0c6ac1 100644 --- a/homeassistant/components/tasmota/translations/he.json +++ b/homeassistant/components/tasmota/translations/he.json @@ -16,5 +16,15 @@ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 Tasmota \u05de\u05e9\u05ea\u05e4\u05d9\u05dd \u05d0\u05ea \u05d4\u05e0\u05d5\u05e9\u05d0 {topic}.\n\n\u05d4\u05ea\u05e7\u05e0\u05d9 Tasmota \u05e2\u05dd \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5: {offenders}.", + "title": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 Tasmota \u05d7\u05d5\u05dc\u05e7\u05d9\u05dd \u05d0\u05ea \u05d0\u05d5\u05ea\u05d5 \u05e0\u05d5\u05e9\u05d0" + }, + "topic_no_prefix": { + "description": "\u05d4\u05ea\u05e7\u05df Tasmota {name} \u05e2\u05dd IP {ip} \u05d0\u05d9\u05e0\u05d5 \u05db\u05d5\u05dc\u05dc `%prefix%` \u05d1\u05de\u05dc\u05d5\u05d0\u05d4.\n\n\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d0\u05dc\u05d4 \u05de\u05d5\u05e9\u05d1\u05ea\u05d5\u05ea \u05e2\u05d3 \u05dc\u05ea\u05d9\u05e7\u05d5\u05df \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4.", + "title": "\u05dc\u05d4\u05ea\u05e7\u05df Tasmota {name} \u05d9\u05e9 \u05e0\u05d5\u05e9\u05d0 MQTT \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/sk.json b/homeassistant/components/tasmota/translations/sk.json new file mode 100644 index 00000000000..c294bc45d7c --- /dev/null +++ b/homeassistant/components/tasmota/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index bbdaa4c8ebb..a77639e3a58 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -6,5 +6,6 @@ "config_flow": true, "codeowners": ["@ludeeus", "@tkdrob"], "iot_class": "local_polling", - "loggers": ["pytautulli"] + "loggers": ["pytautulli"], + "integration_type": "hub" } diff --git a/homeassistant/components/tautulli/translations/bg.json b/homeassistant/components/tautulli/translations/bg.json index 8f8e92fc429..a8cc4448aca 100644 --- a/homeassistant/components/tautulli/translations/bg.json +++ b/homeassistant/components/tautulli/translations/bg.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/tautulli/translations/ca.json b/homeassistant/components/tautulli/translations/ca.json index cc1ea05a46e..b56fb98f2a0 100644 --- a/homeassistant/components/tautulli/translations/ca.json +++ b/homeassistant/components/tautulli/translations/ca.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "El servei ja est\u00e0 configurat", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/tautulli/translations/cs.json b/homeassistant/components/tautulli/translations/cs.json index e65f964f82d..c58c4e4d53d 100644 --- a/homeassistant/components/tautulli/translations/cs.json +++ b/homeassistant/components/tautulli/translations/cs.json @@ -8,6 +8,13 @@ "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" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/de.json b/homeassistant/components/tautulli/translations/de.json index fe6cc4f82ac..874d4c9aaab 100644 --- a/homeassistant/components/tautulli/translations/de.json +++ b/homeassistant/components/tautulli/translations/de.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Der Dienst ist bereits konfiguriert", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -15,7 +14,7 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Um deinen API-Schl\u00fcssel zu finden, \u00f6ffne die Tautulli-Webseite und navigiere zu Einstellungen und dann zu Webinterface. Der API-Schl\u00fcssel befindet sich unten auf dieser Seite.", + "description": "Um deinen API-Schl\u00fcssel zu finden, \u00f6ffne die Tautulli Webseite und navigiere zu Einstellungen und dann zu Webinterface. Der API-Schl\u00fcssel befindet sich unten auf dieser Seite.", "title": "Tautulli erneut authentifizieren" }, "user": { @@ -24,7 +23,7 @@ "url": "URL", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, - "description": "Um deinen API-Schl\u00fcssel zu finden, \u00f6ffne die Tautulli-Webseite und navigiere zu Einstellungen und dann zu Webinterface. Der API-Schl\u00fcssel befindet sich unten auf dieser Seite.\n\nBeispiel f\u00fcr die URL: ```http://192.168.0.10:8181`` mit 8181 als Standard-Port." + "description": "Um deinen API-Schl\u00fcssel zu finden, \u00f6ffne die Tautulli Webseite und navigiere zu Einstellungen und dann zu Webinterface. Der API-Schl\u00fcssel befindet sich unten auf dieser Seite.\n\nBeispiel f\u00fcr die URL: ```http://192.168.0.10:8181`` mit 8181 als Standard-Port." } } } diff --git a/homeassistant/components/tautulli/translations/el.json b/homeassistant/components/tautulli/translations/el.json index 6f105458435..f0cf9de5479 100644 --- a/homeassistant/components/tautulli/translations/el.json +++ b/homeassistant/components/tautulli/translations/el.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/tautulli/translations/en.json b/homeassistant/components/tautulli/translations/en.json index daefd71bf2c..90cb2723ad6 100644 --- a/homeassistant/components/tautulli/translations/en.json +++ b/homeassistant/components/tautulli/translations/en.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Service is already configured", - "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/tautulli/translations/es.json b/homeassistant/components/tautulli/translations/es.json index 2bbdc4facea..e08f94b1d61 100644 --- a/homeassistant/components/tautulli/translations/es.json +++ b/homeassistant/components/tautulli/translations/es.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/tautulli/translations/et.json b/homeassistant/components/tautulli/translations/et.json index 30ef733c976..229a0741bb7 100644 --- a/homeassistant/components/tautulli/translations/et.json +++ b/homeassistant/components/tautulli/translations/et.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", - "reauth_successful": "Taastuvastamine \u00f5nnestus", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/tautulli/translations/fr.json b/homeassistant/components/tautulli/translations/fr.json index ad9c327c551..48b42647484 100644 --- a/homeassistant/components/tautulli/translations/fr.json +++ b/homeassistant/components/tautulli/translations/fr.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/tautulli/translations/he.json b/homeassistant/components/tautulli/translations/he.json index 7091be81520..3718a91d27f 100644 --- a/homeassistant/components/tautulli/translations/he.json +++ b/homeassistant/components/tautulli/translations/he.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/simplisafe/translations/ro.json b/homeassistant/components/tautulli/translations/hr.json similarity index 59% rename from homeassistant/components/simplisafe/translations/ro.json rename to homeassistant/components/tautulli/translations/hr.json index efbbe49f38a..b4c376c3855 100644 --- a/homeassistant/components/simplisafe/translations/ro.json +++ b/homeassistant/components/tautulli/translations/hr.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "password": "Parola", - "username": "Adresa de email" + "api_key": "API klju\u010d" } } } diff --git a/homeassistant/components/tautulli/translations/hu.json b/homeassistant/components/tautulli/translations/hu.json index 7d31ad678f2..176207b53a2 100644 --- a/homeassistant/components/tautulli/translations/hu.json +++ b/homeassistant/components/tautulli/translations/hu.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/tautulli/translations/id.json b/homeassistant/components/tautulli/translations/id.json index 18669b36f29..5b8a6b4201e 100644 --- a/homeassistant/components/tautulli/translations/id.json +++ b/homeassistant/components/tautulli/translations/id.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Layanan sudah dikonfigurasi", - "reauth_successful": "Autentikasi ulang berhasil", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/tautulli/translations/it.json b/homeassistant/components/tautulli/translations/it.json index fcc456a8763..e7bb8dce1ba 100644 --- a/homeassistant/components/tautulli/translations/it.json +++ b/homeassistant/components/tautulli/translations/it.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json index fd51dc92c43..ab5376b83e0 100644 --- a/homeassistant/components/tautulli/translations/ja.json +++ b/homeassistant/components/tautulli/translations/ja.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/tautulli/translations/nl.json b/homeassistant/components/tautulli/translations/nl.json index f01a1fdb17d..e71bba57779 100644 --- a/homeassistant/components/tautulli/translations/nl.json +++ b/homeassistant/components/tautulli/translations/nl.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Dienst is al geconfigureerd", - "reauth_successful": "Herauthenticatie geslaagd", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/tautulli/translations/no.json b/homeassistant/components/tautulli/translations/no.json index 0528a97beb9..27303223573 100644 --- a/homeassistant/components/tautulli/translations/no.json +++ b/homeassistant/components/tautulli/translations/no.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Re-autentisering var vellykket", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/tautulli/translations/pl.json b/homeassistant/components/tautulli/translations/pl.json index 6dac9a79617..49f833f8811 100644 --- a/homeassistant/components/tautulli/translations/pl.json +++ b/homeassistant/components/tautulli/translations/pl.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/tautulli/translations/pt-BR.json b/homeassistant/components/tautulli/translations/pt-BR.json index e5732024e3a..c8d7f364088 100644 --- a/homeassistant/components/tautulli/translations/pt-BR.json +++ b/homeassistant/components/tautulli/translations/pt-BR.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/tautulli/translations/ru.json b/homeassistant/components/tautulli/translations/ru.json index 4f777441385..6db4b0ae948 100644 --- a/homeassistant/components/tautulli/translations/ru.json +++ b/homeassistant/components/tautulli/translations/ru.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "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.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + "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": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/tautulli/translations/sk.json b/homeassistant/components/tautulli/translations/sk.json new file mode 100644 index 00000000000..769b6125412 --- /dev/null +++ b/homeassistant/components/tautulli/translations/sk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "url": "URL", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json index 1d4ad84cf58..4702d828c22 100644 --- a/homeassistant/components/tautulli/translations/sv.json +++ b/homeassistant/components/tautulli/translations/sv.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", - "reauth_successful": "\u00c5terautentisering lyckades", - "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", diff --git a/homeassistant/components/tautulli/translations/tr.json b/homeassistant/components/tautulli/translations/tr.json index e5fd6c14b67..39b9ce97750 100644 --- a/homeassistant/components/tautulli/translations/tr.json +++ b/homeassistant/components/tautulli/translations/tr.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "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." + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/tautulli/translations/zh-Hant.json b/homeassistant/components/tautulli/translations/zh-Hant.json index c21af61cfa5..06df0275ea9 100644 --- a/homeassistant/components/tautulli/translations/zh-Hant.json +++ b/homeassistant/components/tautulli/translations/zh-Hant.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index d77265bb351..78662ca4536 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "So verkn\u00fcpfest du dein TelldusLive-Konto: \n 1. Dr\u00fccke auf den Link unten \n 2. Melde dich bei Telldus Live an \n 3. Autorisiere ** {app_name} ** (dr\u00fccke auf ** Yes **). \n 4. Komme hierher zur\u00fcck und kdr\u00fccke auf **SENDEN**. \n\n [Link TelldusLive-Konto]({auth_url})", + "description": "So verkn\u00fcpfst du dein TelldusLive-Konto: \n 1. Dr\u00fccke auf den Link unten \n 2. Melde dich bei Telldus Live an \n 3. Autorisiere ** {app_name} ** (dr\u00fccke auf ** Yes **). \n 4. Komme hierher zur\u00fcck und kdr\u00fccke auf **SENDEN**. \n\n [Link TelldusLive-Konto]({auth_url})", "title": "Authentifiziere dich gegen TelldusLive" }, "user": { diff --git a/homeassistant/components/tellduslive/translations/hr.json b/homeassistant/components/tellduslive/translations/hr.json new file mode 100644 index 00000000000..4b26b20a8eb --- /dev/null +++ b/homeassistant/components/tellduslive/translations/hr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dekivana gre\u0161ka" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/sk.json b/homeassistant/components/tellduslive/translations/sk.json index 5ada995aa6e..e0af50871da 100644 --- a/homeassistant/components/tellduslive/translations/sk.json +++ b/homeassistant/components/tellduslive/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "unknown_authorize_url_generation": "Nezn\u00e1ma chyba pri generovan\u00ed autorizovanej adresy URL." + }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Pr\u00e1zdne" + } } } } \ No newline at end of file diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index d7c117c9be7..8f164142212 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -189,9 +189,9 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): return self._state @property - def supported_features(self) -> int: + def supported_features(self) -> AlarmControlPanelEntityFeature: """Return the list of supported features.""" - supported_features = 0 + supported_features = AlarmControlPanelEntityFeature(0) if self._arm_night_script is not None: supported_features = ( supported_features | AlarmControlPanelEntityFeature.ARM_NIGHT diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index aad0270e434..4c234e21875 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -309,7 +309,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): return self._device_class @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index b27a6ee3e51..3d3c17551ca 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -147,7 +147,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._preset_mode_template = config.get(CONF_PRESET_MODE_TEMPLATE) self._oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE) self._direction_template = config.get(CONF_DIRECTION_TEMPLATE) - self._supported_features = 0 self._on_script = Script(hass, config[CONF_ON_ACTION], friendly_name, DOMAIN) self._off_script = Script(hass, config[CONF_OFF_ACTION], friendly_name, DOMAIN) @@ -189,18 +188,13 @@ class TemplateFan(TemplateEntity, FanEntity): self._preset_modes = config.get(CONF_PRESET_MODES) if self._percentage_template: - self._supported_features |= FanEntityFeature.SET_SPEED + self._attr_supported_features |= FanEntityFeature.SET_SPEED if self._preset_mode_template and self._preset_modes: - self._supported_features |= FanEntityFeature.PRESET_MODE + self._attr_supported_features |= FanEntityFeature.PRESET_MODE if self._oscillating_template: - self._supported_features |= FanEntityFeature.OSCILLATE + self._attr_supported_features |= FanEntityFeature.OSCILLATE if self._direction_template: - self._supported_features |= FanEntityFeature.DIRECTION - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features + self._attr_supported_features |= FanEntityFeature.DIRECTION @property def speed_count(self) -> int: diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index db1c89921d1..27dcbf0e014 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -254,9 +254,9 @@ class LightTemplate(TemplateEntity, LightEntity): return self._supported_color_modes @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag supported features.""" - supported_features = 0 + supported_features = LightEntityFeature(0) if self._effect_script is not None: supported_features |= LightEntityFeature.EFFECT if self._supports_transition is True: diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9f719b7e1b3..ef02f208e8c 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.23.2", - "pillow==9.2.0" + "pillow==9.3.0" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/components/tesla_wall_connector/translations/sk.json b/homeassistant/components/tesla_wall_connector/translations/sk.json new file mode 100644 index 00000000000..336708e4a3c --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/text/__init__.py b/homeassistant/components/text/__init__.py new file mode 100644 index 00000000000..32054734e8e --- /dev/null +++ b/homeassistant/components/text/__init__.py @@ -0,0 +1,269 @@ +"""Component to allow setting text as platforms.""" +from __future__ import annotations + +from dataclasses import asdict, dataclass +from datetime import timedelta +import logging +import re +from typing import Any, final + +import voluptuous as vol + +from homeassistant.backports.enum import StrEnum +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import MAX_LENGTH_STATE_STATE +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_MAX, + ATTR_MIN, + ATTR_MODE, + ATTR_PATTERN, + ATTR_VALUE, + DOMAIN, + SERVICE_SET_VALUE, +) + +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + +__all__ = ["DOMAIN", "TextEntity", "TextEntityDescription", "TextMode"] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Text entities.""" + component = hass.data[DOMAIN] = EntityComponent[TextEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_SET_VALUE, + {vol.Required(ATTR_VALUE): cv.string}, + _async_set_value, + ) + + return True + + +async def _async_set_value(entity: TextEntity, service_call: ServiceCall) -> None: + """Service call wrapper to set a new value.""" + value = service_call.data[ATTR_VALUE] + if len(value) < entity.min: + raise ValueError( + f"Value {value} for {entity.name} is too short (minimum length {entity.min})" + ) + if len(value) > entity.max: + raise ValueError( + f"Value {value} for {entity.name} is too long (maximum length {entity.max})" + ) + if entity.pattern_cmp and not entity.pattern_cmp.match(value): + raise ValueError( + f"Value {value} for {entity.name} doesn't match pattern {entity.pattern}" + ) + await entity.async_set_value(value) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[TextEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[TextEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +class TextMode(StrEnum): + """Modes for text entities.""" + + PASSWORD = "password" + TEXT = "text" + + +@dataclass +class TextEntityDescription(EntityDescription): + """A class that describes text entities.""" + + native_min: int = 0 + native_max: int = MAX_LENGTH_STATE_STATE + mode: TextMode = TextMode.TEXT + pattern: str | None = None + + +class TextEntity(Entity): + """Representation of a Text entity.""" + + entity_description: TextEntityDescription + _attr_mode: TextMode + _attr_native_value: str | None + _attr_native_min: int + _attr_native_max: int + _attr_pattern: str | None + _attr_state: None = None + __pattern_cmp: re.Pattern | None = None + + @property + def capability_attributes(self) -> dict[str, Any]: + """Return capability attributes.""" + return { + ATTR_MODE: self.mode, + ATTR_MIN: self.min, + ATTR_MAX: self.max, + ATTR_PATTERN: self.pattern, + } + + @property + @final + def state(self) -> str | None: + """Return the entity state.""" + if self.native_value is None: + return None + if len(self.native_value) < self.min: + raise ValueError( + f"Entity {self.entity_id} provides state {self.native_value} which is " + f"too short (minimum length {self.min})" + ) + if len(self.native_value) > self.max: + raise ValueError( + f"Entity {self.entity_id} provides state {self.native_value} which is " + f"too long (maximum length {self.max})" + ) + if self.pattern_cmp and not self.pattern_cmp.match(self.native_value): + raise ValueError( + f"Entity {self.entity_id} provides state {self.native_value} which " + f"does not match expected pattern {self.pattern}" + ) + return self.native_value + + @property + def mode(self) -> TextMode: + """Return the mode of the entity.""" + if hasattr(self, "_attr_mode"): + return self._attr_mode + if hasattr(self, "entity_description"): + return self.entity_description.mode + return TextMode.TEXT + + @property + def native_min(self) -> int: + """Return the minimum length of the value.""" + if hasattr(self, "_attr_native_min"): + return self._attr_native_min + if hasattr(self, "entity_description"): + return self.entity_description.native_min + return 0 + + @property + @final + def min(self) -> int: + """Return the minimum length of the value.""" + return max(self.native_min, 0) + + @property + def native_max(self) -> int: + """Return the maximum length of the value.""" + if hasattr(self, "_attr_native_max"): + return self._attr_native_max + if hasattr(self, "entity_description"): + return self.entity_description.native_max + return MAX_LENGTH_STATE_STATE + + @property + @final + def max(self) -> int: + """Return the maximum length of the value.""" + return min(self.native_max, MAX_LENGTH_STATE_STATE) + + @property + @final + def pattern_cmp(self) -> re.Pattern | None: + """Return a compiled pattern.""" + if self.pattern is None: + self.__pattern_cmp = None + return None + if not self.__pattern_cmp or self.pattern != self.__pattern_cmp.pattern: + self.__pattern_cmp = re.compile(self.pattern) + return self.__pattern_cmp + + @property + def pattern(self) -> str | None: + """Return the regex pattern that the value must match.""" + if hasattr(self, "_attr_pattern"): + return self._attr_pattern + if hasattr(self, "entity_description"): + return self.entity_description.pattern + return None + + @property + def native_value(self) -> str | None: + """Return the value reported by the text.""" + return self._attr_native_value + + def set_value(self, value: str) -> None: + """Change the value.""" + raise NotImplementedError() + + async def async_set_value(self, value: str) -> None: + """Change the value.""" + await self.hass.async_add_executor_job(self.set_value, value) + + +@dataclass +class TextExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + native_value: str | None + native_min: int + native_max: int + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the text data.""" + return asdict(self) + + @classmethod + def from_dict(cls, restored: dict[str, Any]) -> TextExtraStoredData | None: + """Initialize a stored text state from a dict.""" + try: + return cls( + restored["native_value"], + restored["native_min"], + restored["native_max"], + ) + except KeyError: + return None + + +class RestoreText(TextEntity, RestoreEntity): + """Mixin class for restoring previous text state.""" + + @property + def extra_restore_state_data(self) -> TextExtraStoredData: + """Return text specific state data to be restored.""" + return TextExtraStoredData( + self.native_value, + self.native_min, + self.native_max, + ) + + async def async_get_last_text_data(self) -> TextExtraStoredData | None: + """Restore attributes.""" + if (restored_last_extra_data := await self.async_get_last_extra_data()) is None: + return None + return TextExtraStoredData.from_dict(restored_last_extra_data.as_dict()) diff --git a/homeassistant/components/text/const.py b/homeassistant/components/text/const.py new file mode 100644 index 00000000000..3670c30120b --- /dev/null +++ b/homeassistant/components/text/const.py @@ -0,0 +1,11 @@ +"""Provides the constants needed for the component.""" + +DOMAIN = "text" + +ATTR_MAX = "max" +ATTR_MIN = "min" +ATTR_MODE = "mode" +ATTR_PATTERN = "pattern" +ATTR_VALUE = "value" + +SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/text/device_action.py b/homeassistant/components/text/device_action.py new file mode 100644 index 00000000000..3d14da9bdb8 --- /dev/null +++ b/homeassistant/components/text/device_action.py @@ -0,0 +1,80 @@ +"""Provides device actions for Text.""" +from __future__ import annotations + +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 homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from .const import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE + +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(ATTR_VALUE): cv.string, + } +) + + +async def async_get_actions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device actions for Text.""" + registry = entity_registry.async_get(hass) + actions: list[dict[str, str]] = [] + + # 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: ConfigType, + variables: TemplateVarsType, + context: Context | None, +) -> None: + """Execute a device action.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: config[CONF_ENTITY_ID], + ATTR_VALUE: config[ATTR_VALUE], + }, + blocking=True, + context=context, + ) + + +async def async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List action capabilities.""" + fields = {vol.Required(ATTR_VALUE): cv.string} + + return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/text/manifest.json b/homeassistant/components/text/manifest.json new file mode 100644 index 00000000000..3e45499302a --- /dev/null +++ b/homeassistant/components/text/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "text", + "name": "Text", + "documentation": "https://www.home-assistant.io/integrations/text", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal", + "integration_type": "entity" +} diff --git a/homeassistant/components/text/recorder.py b/homeassistant/components/text/recorder.py new file mode 100644 index 00000000000..09642eb3079 --- /dev/null +++ b/homeassistant/components/text/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_PATTERN + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return {ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_PATTERN} diff --git a/homeassistant/components/text/reproduce_state.py b/homeassistant/components/text/reproduce_state.py new file mode 100644 index 00000000000..99013a63a06 --- /dev/null +++ b/homeassistant/components/text/reproduce_state.py @@ -0,0 +1,57 @@ +"""Reproduce a Text entity state.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +import logging +from typing import Any + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, HomeAssistant, State + +from .const import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistant, + state: State, + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce a single state.""" + if (cur_state := hass.states.get(state.entity_id)) is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistant, + states: Iterable[State], + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce multiple Text states.""" + # Reproduce states in parallel. + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/homeassistant/components/text/services.yaml b/homeassistant/components/text/services.yaml new file mode 100644 index 00000000000..00dd0ecafd2 --- /dev/null +++ b/homeassistant/components/text/services.yaml @@ -0,0 +1,14 @@ +set_value: + name: Set value + description: Set value of a text entity. + target: + entity: + domain: text + fields: + value: + name: Value + description: Value to set. + required: true + example: "Hello world!" + selector: + text: diff --git a/homeassistant/components/text/strings.json b/homeassistant/components/text/strings.json new file mode 100644 index 00000000000..0f5ddf5b331 --- /dev/null +++ b/homeassistant/components/text/strings.json @@ -0,0 +1,8 @@ +{ + "title": "Text", + "device_automation": { + "action_type": { + "set_value": "Set value for {entity_name}" + } + } +} diff --git a/homeassistant/components/text/translations/bg.json b/homeassistant/components/text/translations/bg.json new file mode 100644 index 00000000000..87a2a7629be --- /dev/null +++ b/homeassistant/components/text/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0422\u0435\u043a\u0441\u0442" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/ca.json b/homeassistant/components/text/translations/ca.json new file mode 100644 index 00000000000..19b840cdb8f --- /dev/null +++ b/homeassistant/components/text/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Text" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/cs.json b/homeassistant/components/text/translations/cs.json new file mode 100644 index 00000000000..19b840cdb8f --- /dev/null +++ b/homeassistant/components/text/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "Text" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/de.json b/homeassistant/components/text/translations/de.json new file mode 100644 index 00000000000..19b840cdb8f --- /dev/null +++ b/homeassistant/components/text/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Text" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/el.json b/homeassistant/components/text/translations/el.json new file mode 100644 index 00000000000..89853ef69f6 --- /dev/null +++ b/homeassistant/components/text/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u039a\u03b5\u03af\u03bc\u03b5\u03bd\u03bf" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/en.json b/homeassistant/components/text/translations/en.json new file mode 100644 index 00000000000..5d0d038c05c --- /dev/null +++ b/homeassistant/components/text/translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Set value for {entity_name}" + } + }, + "title": "Text" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/es.json b/homeassistant/components/text/translations/es.json new file mode 100644 index 00000000000..07996c47b61 --- /dev/null +++ b/homeassistant/components/text/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Texto" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/et.json b/homeassistant/components/text/translations/et.json new file mode 100644 index 00000000000..6ac4d0d7b31 --- /dev/null +++ b/homeassistant/components/text/translations/et.json @@ -0,0 +1,3 @@ +{ + "title": "Tekst" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/fr.json b/homeassistant/components/text/translations/fr.json new file mode 100644 index 00000000000..cdb99210efe --- /dev/null +++ b/homeassistant/components/text/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Texte" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/id.json b/homeassistant/components/text/translations/id.json new file mode 100644 index 00000000000..ca3ba59d316 --- /dev/null +++ b/homeassistant/components/text/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Teks" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/nl.json b/homeassistant/components/text/translations/nl.json new file mode 100644 index 00000000000..6ac4d0d7b31 --- /dev/null +++ b/homeassistant/components/text/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Tekst" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/no.json b/homeassistant/components/text/translations/no.json new file mode 100644 index 00000000000..6ac4d0d7b31 --- /dev/null +++ b/homeassistant/components/text/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Tekst" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/pt-BR.json b/homeassistant/components/text/translations/pt-BR.json new file mode 100644 index 00000000000..07996c47b61 --- /dev/null +++ b/homeassistant/components/text/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Texto" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/ru.json b/homeassistant/components/text/translations/ru.json new file mode 100644 index 00000000000..87a2a7629be --- /dev/null +++ b/homeassistant/components/text/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u0422\u0435\u043a\u0441\u0442" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/zh-Hans.json b/homeassistant/components/text/translations/zh-Hans.json new file mode 100644 index 00000000000..5c958498042 --- /dev/null +++ b/homeassistant/components/text/translations/zh-Hans.json @@ -0,0 +1,3 @@ +{ + "title": "\u6587\u672c" +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/zh-Hant.json b/homeassistant/components/text/translations/zh-Hant.json new file mode 100644 index 00000000000..b6aaf2f6191 --- /dev/null +++ b/homeassistant/components/text/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "\u6587\u5b57" +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/device.py b/homeassistant/components/thermobeacon/device.py index 327a206042a..fe8a499d6ed 100644 --- a/homeassistant/components/thermobeacon/device.py +++ b/homeassistant/components/thermobeacon/device.py @@ -1,13 +1,11 @@ """Support for ThermoBeacon devices.""" from __future__ import annotations -from thermobeacon_ble import DeviceKey, SensorDeviceInfo +from thermobeacon_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo def device_key_to_bluetooth_entity_key( @@ -15,17 +13,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a thermobeacon device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 34321a66681..3e105eff138 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -22,9 +22,15 @@ "manufacturer_data_start": [0], "connectable": false }, + { + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 24, + "manufacturer_data_start": [0], + "connectable": false + }, { "local_name": "ThermoBeacon", "connectable": false } ], - "requirements": ["thermobeacon-ble==0.3.2"], + "requirements": ["thermobeacon-ble==0.4.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/components/thermobeacon/sensor.py b/homeassistant/components/thermobeacon/sensor.py index 83b616f8d84..900aacbf7a7 100644 --- a/homeassistant/components/thermobeacon/sensor.py +++ b/homeassistant/components/thermobeacon/sensor.py @@ -31,9 +31,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { (ThermoBeaconSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( @@ -87,7 +88,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/thermobeacon/translations/he.json b/homeassistant/components/thermobeacon/translations/he.json index b182a698234..e34a0c9d525 100644 --- a/homeassistant/components/thermobeacon/translations/he.json +++ b/homeassistant/components/thermobeacon/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" }, "flow_title": "{name}", diff --git a/homeassistant/components/thermobeacon/translations/sk.json b/homeassistant/components/thermobeacon/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/sensor.py b/homeassistant/components/thermopro/sensor.py index 505f620229c..22a6e2f086a 100644 --- a/homeassistant/components/thermopro/sensor.py +++ b/homeassistant/components/thermopro/sensor.py @@ -6,7 +6,6 @@ from typing import Optional, Union from thermopro_ble import ( DeviceKey, SensorDeviceClass as ThermoProSensorDeviceClass, - SensorDeviceInfo, SensorUpdate, Units, ) @@ -26,16 +25,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN @@ -75,27 +71,13 @@ def _device_key_to_bluetooth_entity_key( return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/thermopro/translations/he.json b/homeassistant/components/thermopro/translations/he.json index 47308062d0d..26219169d12 100644 --- a/homeassistant/components/thermopro/translations/he.json +++ b/homeassistant/components/thermopro/translations/he.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/thermopro/translations/sk.json b/homeassistant/components/thermopro/translations/sk.json new file mode 100644 index 00000000000..b121bbc35a3 --- /dev/null +++ b/homeassistant/components/thermopro/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index e3af2e9c567..fbb12872306 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -6,23 +6,26 @@ from typing import Any import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_ENTITY_ID, CONF_NAME from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, SchemaConfigFlowHandler, SchemaFlowError, SchemaFlowFormStep, - SchemaFlowMenuStep, ) from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, DOMAIN -def _validate_mode(data: Any) -> Any: +async def _validate_mode( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: """Validate the threshold mode, and set limits to None if not set.""" - if CONF_LOWER not in data and CONF_UPPER not in data: + if CONF_LOWER not in user_input and CONF_UPPER not in user_input: raise SchemaFlowError("need_lower_upper") - return {CONF_LOWER: None, CONF_UPPER: None, **data} + return {CONF_LOWER: None, CONF_UPPER: None, **user_input} OPTIONS_SCHEMA = vol.Schema( @@ -51,16 +54,16 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): selector.TextSelector(), vol.Required(CONF_ENTITY_ID): selector.EntitySelector( - selector.EntitySelectorConfig(domain="sensor") + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) ), } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { +CONFIG_FLOW = { "user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { +OPTIONS_FLOW = { "init": SchemaFlowFormStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) } diff --git a/homeassistant/components/threshold/translations/de.json b/homeassistant/components/threshold/translations/de.json index f8f805742ca..9a8d650482c 100644 --- a/homeassistant/components/threshold/translations/de.json +++ b/homeassistant/components/threshold/translations/de.json @@ -12,7 +12,7 @@ "name": "Name", "upper": "Obergrenze" }, - "description": "Erstellen eines bin\u00e4ren Sensors, der sich je nach Wert eines Sensors ein- und ausschaltet\n\nNur unterer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors kleiner als der untere Grenzwert ist.\nNur oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors gr\u00f6\u00dfer als der obere Grenzwert ist.\nSowohl untere als auch obere Grenze konfiguriert - Einschalten, wenn der Wert des Eingangssensors im Bereich [untere Grenze ... obere Grenze] liegt.", + "description": "Erstellen eines bin\u00e4ren Sensors, der sich je nach Wert eines Sensors ein- und ausschaltet\n\nNur unterer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors kleiner als der untere Grenzwert ist.\nNur oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors gr\u00f6\u00dfer als der obere Grenzwert ist.\nSowohl untere als auch obere Grenze konfiguriert - Einschalten, wenn der Wert des Eingangssensors im Bereich [untere Grenze \u2026 obere Grenze] liegt.", "title": "Schwellenwertsensor hinzuf\u00fcgen" } } @@ -30,7 +30,7 @@ "name": "Name", "upper": "Obergrenze" }, - "description": "Nur unterer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors kleiner als der untere Grenzwert ist.\nNur oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors gr\u00f6\u00dfer als der obere Grenzwert ist.\nSowohl unterer als auch oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors im Bereich [unterer Grenzwert ... oberer Grenzwert] liegt." + "description": "Nur unterer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors kleiner als der untere Grenzwert ist.\nNur oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors gr\u00f6\u00dfer als der obere Grenzwert ist.\nSowohl unterer als auch oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors im Bereich [unterer Grenzwert \u2026 oberer Grenzwert] liegt." } } }, diff --git a/homeassistant/components/switchbot/translations/pt.json b/homeassistant/components/threshold/translations/sk.json similarity index 66% rename from homeassistant/components/switchbot/translations/pt.json rename to homeassistant/components/threshold/translations/sk.json index e2dc9fee9b4..fcfc77b7cf2 100644 --- a/homeassistant/components/switchbot/translations/pt.json +++ b/homeassistant/components/threshold/translations/sk.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "password": "Palavra-passe" + "entity_id": "Vstupn\u00fd sn\u00edma\u010d" } } } @@ -12,7 +12,7 @@ "step": { "init": { "data": { - "update_time": "Tempo entre actualiza\u00e7\u00f5es (segundos)" + "entity_id": "Vstupn\u00fd sn\u00edma\u010d" } } } diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 892b47f39ca..05bfe96b071 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.26.1"], + "requirements": ["pyTibber==0.26.4"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 4dcc4a8a777..31106990a03 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -350,6 +350,7 @@ class TibberSensorElPrice(TibberSensor): } self._attr_icon = ICON self._attr_name = f"Electricity price {self._home_name}" + self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_unique_id = self._tibber_home.home_id self._model = "Price Sensor" @@ -590,7 +591,7 @@ class TibberDataCoordinator(DataUpdateCoordinator): ) last_stats = await get_instance(self.hass).async_add_executor_job( - get_last_statistics, self.hass, 1, statistic_id, True + get_last_statistics, self.hass, 1, statistic_id, True, {} ) if not last_stats: @@ -623,7 +624,8 @@ class TibberDataCoordinator(DataUpdateCoordinator): None, [statistic_id], "hour", - True, + None, + {"sum"}, ) _sum = stat[statistic_id][0]["sum"] last_stats_time = stat[statistic_id][0]["start"] diff --git a/homeassistant/components/tibber/translations/sk.json b/homeassistant/components/tibber/translations/sk.json index 13ca7333f5c..bb83eca3402 100644 --- a/homeassistant/components/tibber/translations/sk.json +++ b/homeassistant/components/tibber/translations/sk.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "timeout": "Pri prip\u00e1jan\u00ed k Tibberu vypr\u0161al \u010dasov\u00fd limit" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 86afee18505..7931bbb8797 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -5,8 +5,11 @@ import logging from pytile.tile import Tile -from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import ( + AsyncSeeCallback, + SourceType, + TrackerEntity, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/tile/translations/bg.json b/homeassistant/components/tile/translations/bg.json index 08a4edb4db8..b1b1faed2fe 100644 --- a/homeassistant/components/tile/translations/bg.json +++ b/homeassistant/components/tile/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/tile/translations/sk.json b/homeassistant/components/tile/translations/sk.json index d30ed436a4f..c14faaae46e 100644 --- a/homeassistant/components/tile/translations/sk.json +++ b/homeassistant/components/tile/translations/sk.json @@ -1,14 +1,21 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { + "password": "Heslo", "username": "Email" } } diff --git a/homeassistant/components/tilt_ble/sensor.py b/homeassistant/components/tilt_ble/sensor.py index 54d05b3c900..bddae2ca027 100644 --- a/homeassistant/components/tilt_ble/sensor.py +++ b/homeassistant/components/tilt_ble/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from tilt_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from tilt_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -19,16 +19,10 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_FAHRENHEIT, -) +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN @@ -63,27 +57,13 @@ def _device_key_to_bluetooth_entity_key( return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/tilt_ble/translations/he.json b/homeassistant/components/tilt_ble/translations/he.json index de780eb221a..26219169d12 100644 --- a/homeassistant/components/tilt_ble/translations/he.json +++ b/homeassistant/components/tilt_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/tilt_ble/translations/sk.json b/homeassistant/components/tilt_ble/translations/sk.json new file mode 100644 index 00000000000..b121bbc35a3 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/timer/translations/de.json b/homeassistant/components/timer/translations/de.json index ba24845aadb..daf3df59514 100644 --- a/homeassistant/components/timer/translations/de.json +++ b/homeassistant/components/timer/translations/de.json @@ -2,7 +2,7 @@ "state": { "_": { "active": "Aktiv", - "idle": "Unt\u00e4tig", + "idle": "Inaktiv", "paused": "Pausiert" } } diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 1c909388b60..e3a40be16c0 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_AFTER, CONF_BEFORE, CONF_NAME, + CONF_UNIQUE_ID, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) @@ -43,6 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period, vol.Optional(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -85,7 +87,8 @@ async def async_setup_platform( before = config[CONF_BEFORE] before_offset = config[CONF_BEFORE_OFFSET] name = config[CONF_NAME] - sensor = TodSensor(name, after, after_offset, before, before_offset, None) + unique_id = config.get(CONF_UNIQUE_ID) + sensor = TodSensor(name, after, after_offset, before, before_offset, unique_id) async_add_entities([sensor]) diff --git a/homeassistant/components/tod/config_flow.py b/homeassistant/components/tod/config_flow.py index 5155d15561b..6e21b8046a1 100644 --- a/homeassistant/components/tod/config_flow.py +++ b/homeassistant/components/tod/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaConfigFlowHandler, SchemaFlowFormStep, - SchemaFlowMenuStep, ) from .const import CONF_AFTER_TIME, CONF_BEFORE_TIME, DOMAIN @@ -29,12 +28,12 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "user": SchemaFlowFormStep(CONFIG_SCHEMA) +CONFIG_FLOW = { + "user": SchemaFlowFormStep(CONFIG_SCHEMA), } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), } diff --git a/homeassistant/components/tod/translations/de.json b/homeassistant/components/tod/translations/de.json index 663dc21c993..9907cdd3406 100644 --- a/homeassistant/components/tod/translations/de.json +++ b/homeassistant/components/tod/translations/de.json @@ -22,5 +22,5 @@ } } }, - "title": "Tageszeitensensor" + "title": "Tageszeitsensor" } \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/sk.json b/homeassistant/components/tod/translations/sk.json similarity index 63% rename from homeassistant/components/hangouts/translations/sk.json rename to homeassistant/components/tod/translations/sk.json index 45123261c43..af15f92c2f2 100644 --- a/homeassistant/components/hangouts/translations/sk.json +++ b/homeassistant/components/tod/translations/sk.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "email": "Email", - "password": "Heslo" + "name": "N\u00e1zov" } } } diff --git a/homeassistant/components/tolo/translations/ru.json b/homeassistant/components/tolo/translations/ru.json index 0243a40cf7e..e9ff9c6552f 100644 --- a/homeassistant/components/tolo/translations/ru.json +++ b/homeassistant/components/tolo/translations/ru.json @@ -15,7 +15,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." } } } diff --git a/homeassistant/components/tolo/translations/sk.json b/homeassistant/components/tolo/translations/sk.json new file mode 100644 index 00000000000..dc2e106293a --- /dev/null +++ b/homeassistant/components/tolo/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Zadajte n\u00e1zov hostite\u013ea alebo IP adresu v\u00e1\u0161ho zariadenia TOLO Sauna." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py index a6af4ef5819..4b1e2487da8 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -10,7 +10,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_FOG, ATTR_CONDITION_HAIL, - ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, ATTR_CONDITION_PARTLYCLOUDY, ATTR_CONDITION_POURING, ATTR_CONDITION_RAINY, @@ -63,9 +63,9 @@ CONDITIONS = { WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY, WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY, WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY, - WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING, - WeatherCode.RAIN: ATTR_CONDITION_POURING, - WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING_RAINY, + WeatherCode.HEAVY_RAIN: ATTR_CONDITION_POURING, + WeatherCode.RAIN: ATTR_CONDITION_RAINY, WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY, WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY, WeatherCode.FOG: ATTR_CONDITION_FOG, diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 07b922e72ed..a174d983131 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -26,13 +26,11 @@ from homeassistant.const import ( CONF_NAME, IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, IRRADIATION_WATTS_PER_SQUARE_METER, - LENGTH_KILOMETERS, - LENGTH_MILES, PERCENTAGE, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -103,20 +101,20 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_FEELS_LIKE, name="Feels Like", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), TomorrowioSensorEntityDescription( key=TMRW_ATTR_DEW_POINT, name="Dew Point", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), # Data comes in as hPa TomorrowioSensorEntityDescription( key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL, name="Pressure (Surface Level)", - native_unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, ), # Data comes in as W/m^2, convert to BTUs/(hr * ft^2) for imperial @@ -132,20 +130,24 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_BASE, name="Cloud Base", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, + unit_imperial=UnitOfLength.MILES, + unit_metric=UnitOfLength.KILOMETERS, imperial_conversion=lambda val: DistanceConverter.convert( - val, LENGTH_KILOMETERS, LENGTH_MILES + val, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, ), ), # Data comes in as km, convert to miles for imperial TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_CEILING, name="Cloud Ceiling", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, + unit_imperial=UnitOfLength.MILES, + unit_metric=UnitOfLength.KILOMETERS, imperial_conversion=lambda val: DistanceConverter.convert( - val, LENGTH_KILOMETERS, LENGTH_MILES + val, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, ), ), TomorrowioSensorEntityDescription( @@ -157,10 +159,10 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_WIND_GUST, name="Wind Gust", - unit_imperial=SPEED_MILES_PER_HOUR, - unit_metric=SPEED_METERS_PER_SECOND, + unit_imperial=UnitOfSpeed.MILES_PER_HOUR, + unit_metric=UnitOfSpeed.METERS_PER_SECOND, imperial_conversion=lambda val: SpeedConverter.convert( - val, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR + val, UnitOfSpeed.METERS_PER_SECOND, UnitOfSpeed.MILES_PER_HOUR ), ), TomorrowioSensorEntityDescription( diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json index 27eeea44515..c18fdffebbc 100644 --- a/homeassistant/components/tomorrowio/translations/es.json +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -23,7 +23,7 @@ "data": { "timestep": "Min. entre previsiones de NowCast" }, - "description": "Si eliges habilitar la entidad de pron\u00f3stico del tiempo `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", + "description": "Si eliges habilitar la entidad de previsi\u00f3n `nowcast`, puedes configurar la cantidad de minutos entre cada previsi\u00f3n. La cantidad de previsiones proporcionadas depende de la cantidad de minutos elegidos entre las mismas.", "title": "Actualizar las opciones de Tomorrow.io" } } diff --git a/homeassistant/components/tomorrowio/translations/sensor.sk.json b/homeassistant/components/tomorrowio/translations/sensor.sk.json index 3dd3dede27b..45600dcb861 100644 --- a/homeassistant/components/tomorrowio/translations/sensor.sk.json +++ b/homeassistant/components/tomorrowio/translations/sensor.sk.json @@ -1,7 +1,15 @@ { "state": { "tomorrowio__pollen_index": { - "low": "N\u00edzka" + "high": "Vysok\u00fd", + "low": "N\u00edzka", + "medium": "Stredn\u00fd" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Mrzn\u00faci d\u00e1\u017e\u010f", + "ice_pellets": "\u013dadovec", + "rain": "D\u00e1\u017e\u010f", + "snow": "Sneh" } } } \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sk.json b/homeassistant/components/tomorrowio/translations/sk.json index 7f480c9778c..15048d51d1e 100644 --- a/homeassistant/components/tomorrowio/translations/sk.json +++ b/homeassistant/components/tomorrowio/translations/sk.json @@ -1,11 +1,19 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "rate_limited": "Moment\u00e1lne je r\u00fdchlos\u0165 obmedzen\u00e1, sk\u00faste to pros\u00edm nesk\u00f4r.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", + "location": "Umiestnenie", "name": "Meno" - } + }, + "description": "Ak chcete z\u00edska\u0165 k\u013e\u00fa\u010d API, zaregistrujte sa na [Tomorrow.io] (https://app.tomorrow.io/signup)." } } } diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 07ea079b1ce..d92ac401f92 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -21,11 +21,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_NAME, - LENGTH_KILOMETERS, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -74,11 +74,11 @@ async def async_setup_entry( class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_visibility_unit = LENGTH_KILOMETERS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_visibility_unit = UnitOfLength.KILOMETERS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__( self, diff --git a/homeassistant/components/toon/translations/sk.json b/homeassistant/components/toon/translations/sk.json new file mode 100644 index 00000000000..7ff07bff835 --- /dev/null +++ b/homeassistant/components/toon/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Vybran\u00e1 dohoda je u\u017e nakonfigurovan\u00e1.", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "unknown_authorize_url_generation": "Nezn\u00e1ma chyba pri generovan\u00ed autorizovanej adresy URL." + }, + "step": { + "agreement": { + "data": { + "agreement": "Dohoda" + }, + "description": "Vyberte adresu zmluvy, ktor\u00fa chcete prida\u0165.", + "title": "Vyberte svoju dohodu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/bg.json b/homeassistant/components/totalconnect/translations/bg.json index e5aed3bb504..83bf8d48400 100644 --- a/homeassistant/components/totalconnect/translations/bg.json +++ b/homeassistant/components/totalconnect/translations/bg.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "locations": { diff --git a/homeassistant/components/totalconnect/translations/sk.json b/homeassistant/components/totalconnect/translations/sk.json index 71a7aea5018..e3b18794804 100644 --- a/homeassistant/components/totalconnect/translations/sk.json +++ b/homeassistant/components/totalconnect/translations/sk.json @@ -1,10 +1,29 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "usercode": "Pou\u017e\u00edvate\u013esk\u00fd k\u00f3d nie je platn\u00fd pre tohto pou\u017e\u00edvate\u013ea na tomto mieste" + }, + "step": { + "locations": { + "data": { + "usercode": "Pou\u017e\u00edvate\u013esk\u00fd k\u00f3d" + }, + "description": "Zadajte pou\u017e\u00edvate\u013esk\u00fd k\u00f3d pre tohto pou\u017e\u00edvate\u013ea na adrese {location_id}" + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index f946629813e..c8944b7add2 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -269,7 +269,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): @property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" - return round((self.device.brightness * 255.0) / 100.0) + return round((cast(int, self.device.brightness) * 255.0) / 100.0) @property def hs_color(self) -> tuple[int, int] | None: @@ -312,7 +312,7 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): device: SmartLightStrip @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag supported features.""" return super().supported_features | LightEntityFeature.EFFECT diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index fc44b0d7ae7..8a4dcfb5134 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/tplink/translations/sk.json b/homeassistant/components/tplink/translations/sk.json new file mode 100644 index 00000000000..db96518e108 --- /dev/null +++ b/homeassistant/components/tplink/translations/sk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Zariadenie" + } + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 1f6b0b828bd..601229ed2e0 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -22,8 +22,8 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, AsyncSeeCallback, SourceType, + TrackerEntity, ) -from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EVENT, diff --git a/homeassistant/components/traccar/translations/sk.json b/homeassistant/components/traccar/translations/sk.json new file mode 100644 index 00000000000..933f73976d2 --- /dev/null +++ b/homeassistant/components/traccar/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index aa5048b89c0..cc0b2b2b6cb 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await client.close() raise ConfigEntryNotReady from error - tractive = TractiveClient(hass, client, creds["user_id"]) + tractive = TractiveClient(hass, client, creds["user_id"], entry) tractive.subscribe() try: @@ -148,7 +148,11 @@ class TractiveClient: """A Tractive client.""" def __init__( - self, hass: HomeAssistant, client: aiotractive.Tractive, user_id: str + self, + hass: HomeAssistant, + client: aiotractive.Tractive, + user_id: str, + config_entry: ConfigEntry, ) -> None: """Initialize the client.""" self._hass = hass @@ -157,6 +161,7 @@ class TractiveClient: self._last_hw_time = 0 self._last_pos_time = 0 self._listen_task: asyncio.Task | None = None + self._config_entry = config_entry @property def user_id(self) -> str: @@ -210,6 +215,15 @@ class TractiveClient: ): self._last_pos_time = event["position"]["time"] self._send_position_update(event) + except aiotractive.exceptions.UnauthorizedError: + self._config_entry.async_start_reauth(self._hass) + await self.unsubscribe() + _LOGGER.error( + "Authentication failed for %s, try reconfiguring device", + self._config_entry.data[CONF_EMAIL], + ) + return + except aiotractive.exceptions.TractiveError: _LOGGER.debug( "Tractive is not available. Internet connection is down? Sleeping %i seconds and retrying", diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index f92a8e71df3..0cb73723369 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 308f190a063..496c69ddcf7 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -3,7 +3,7 @@ "name": "Tractive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tractive", - "requirements": ["aiotractive==0.5.4"], + "requirements": ["aiotractive==0.5.5"], "codeowners": ["@Danielhiversen", "@zhulik", "@bieniu"], "iot_class": "cloud_push", "loggers": ["aiotractive"], diff --git a/homeassistant/components/tractive/translations/bg.json b/homeassistant/components/tractive/translations/bg.json index 0276f3b11cd..7f4cf185d8c 100644 --- a/homeassistant/components/tractive/translations/bg.json +++ b/homeassistant/components/tractive/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/tractive/translations/sensor.sk.json b/homeassistant/components/tractive/translations/sensor.sk.json new file mode 100644 index 00000000000..547d8fcc7c9 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "tractive__tracker_state": { + "system_startup": "Spustenie syst\u00e9mu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sk.json b/homeassistant/components/tractive/translations/sk.json index f8b6dfeea81..6a01efb5eb7 100644 --- a/homeassistant/components/tractive/translations/sk.json +++ b/homeassistant/components/tractive/translations/sk.json @@ -1,15 +1,18 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" } } } diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index ebd90333292..acda0bec06d 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -54,6 +54,7 @@ class TradfriSensorEntityDescription( def _get_air_quality(device: Device) -> int | None: """Fetch the air quality value.""" + assert device.air_purifier_control is not None if ( device.air_purifier_control.air_purifiers[0].air_quality == 65535 ): # The sensor returns 65535 if the fan is turned off @@ -64,8 +65,12 @@ def _get_air_quality(device: Device) -> int | None: def _get_filter_time_left(device: Device) -> int: """Fetch the filter's remaining life (in hours).""" + assert device.air_purifier_control is not None return round( - device.air_purifier_control.air_purifiers[0].filter_lifetime_remaining / 60 + cast( + int, device.air_purifier_control.air_purifiers[0].filter_lifetime_remaining + ) + / 60 ) diff --git a/homeassistant/components/tradfri/translations/hr.json b/homeassistant/components/tradfri/translations/hr.json index bb242ca60f0..32fb3fd7fb4 100644 --- a/homeassistant/components/tradfri/translations/hr.json +++ b/homeassistant/components/tradfri/translations/hr.json @@ -1,13 +1,20 @@ { "config": { "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", "already_in_progress": "Konfiguracija premosnice je ve\u0107 u tijeku." }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + }, "step": { "auth": { "data": { - "host": "Host" - } + "host": "Host", + "security_code": "Sigurnosni kod" + }, + "description": "Sigurnosni k\u00f4d mo\u017eete prona\u0107i na pole\u0111ini pristupnika.", + "title": "Unesite sigurnosni kod" } } } diff --git a/homeassistant/components/tradfri/translations/sk.json b/homeassistant/components/tradfri/translations/sk.json index 299acb612fb..55ad1f9e49d 100644 --- a/homeassistant/components/tradfri/translations/sk.json +++ b/homeassistant/components/tradfri/translations/sk.json @@ -3,6 +3,19 @@ "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "error": { + "cannot_authenticate": "Nemo\u017eno overi\u0165, je br\u00e1na sp\u00e1rovan\u00e1 s in\u00fdm serverom, ako je napr\u00edklad Homekit?", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "auth": { + "data": { + "host": "Hostite\u013e", + "security_code": "Bezpe\u010dnostn\u00fd k\u00f3d" + }, + "title": "Zadajte bezpe\u010dnostn\u00fd k\u00f3d" + } } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index 47b5784296d..26f395debb9 100644 --- a/homeassistant/components/trafikverket_ferry/manifest.json +++ b/homeassistant/components/trafikverket_ferry/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_ferry", "name": "Trafikverket Ferry", "documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry", - "requirements": ["pytrafikverket==0.2.1"], + "requirements": ["pytrafikverket==0.2.2"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_ferry/translations/bg.json b/homeassistant/components/trafikverket_ferry/translations/bg.json index 72b2aa2cf7b..05da54a248e 100644 --- a/homeassistant/components/trafikverket_ferry/translations/bg.json +++ b/homeassistant/components/trafikverket_ferry/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/trafikverket_ferry/translations/sk.json b/homeassistant/components/trafikverket_ferry/translations/sk.json new file mode 100644 index 00000000000..6c2b006092a --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/sk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "incorrect_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d pre vybran\u00fd \u00fa\u010det", + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "time": "\u010cas", + "weekday": "Pracovn\u00e9 dni" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index d8ccd62f956..16a125f9561 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_train", "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", - "requirements": ["pytrafikverket==0.2.1"], + "requirements": ["pytrafikverket==0.2.2"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_train/translations/bg.json b/homeassistant/components/trafikverket_train/translations/bg.json index 91bb9c04e35..86ad46927ae 100644 --- a/homeassistant/components/trafikverket_train/translations/bg.json +++ b/homeassistant/components/trafikverket_train/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/trafikverket_train/translations/cs.json b/homeassistant/components/trafikverket_train/translations/cs.json index 89ded7d388c..8d62fe5c226 100644 --- a/homeassistant/components/trafikverket_train/translations/cs.json +++ b/homeassistant/components/trafikverket_train/translations/cs.json @@ -7,6 +7,13 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "weekday": "Dny" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/sk.json b/homeassistant/components/trafikverket_train/translations/sk.json new file mode 100644 index 00000000000..f04bf4d5e32 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/sk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "incorrect_api_key": "Neplatn\u00fd k\u013e\u00fa\u010d API pre vybran\u00e9 konto" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "weekday": "Dni" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index fbe2435f841..a1d9ca21c02 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_weatherstation", "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", - "requirements": ["pytrafikverket==0.2.1"], + "requirements": ["pytrafikverket==0.2.2"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_weatherstation/translations/bg.json b/homeassistant/components/trafikverket_weatherstation/translations/bg.json index c4f6c0a2f55..bc91923312f 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/bg.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/bg.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "user": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/sk.json b/homeassistant/components/trafikverket_weatherstation/translations/sk.json index ff853127803..46e0695e20d 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/sk.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/sk.json @@ -1,12 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_station": "Nepodarilo sa n\u00e1js\u0165 meteorologick\u00fa stanicu so zadan\u00fdm n\u00e1zvom", + "more_stations": "Na\u0161lo sa viacero meteorologick\u00fdch stan\u00edc so zadan\u00fdm n\u00e1zvom" }, "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d" + "api_key": "API k\u013e\u00fa\u010d", + "station": "Stanica" } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 48d4a0e5163..8f81382012d 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -3,12 +3,13 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import transmissionrpc from transmissionrpc.error import TransmissionError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -21,13 +22,15 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from .const import ( ATTR_DELETE_DATA, ATTR_TORRENT, + CONF_ENTRY_ID, CONF_LIMIT, CONF_ORDER, DATA_UPDATED, @@ -49,30 +52,41 @@ from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -SERVICE_ADD_TORRENT_SCHEMA = vol.Schema( - {vol.Required(ATTR_TORRENT): cv.string, vol.Required(CONF_NAME): cv.string} -) - -SERVICE_REMOVE_TORRENT_SCHEMA = vol.Schema( +SERVICE_BASE_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ID): cv.positive_int, - vol.Optional(ATTR_DELETE_DATA, default=DEFAULT_DELETE_DATA): cv.boolean, + vol.Exclusive(CONF_ENTRY_ID, "identifier"): selector.ConfigEntrySelector(), + vol.Exclusive(CONF_NAME, "identifier"): selector.TextSelector(), } ) -SERVICE_START_TORRENT_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ID): cv.positive_int, - } +SERVICE_ADD_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend({vol.Required(ATTR_TORRENT): cv.string}), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), ) -SERVICE_STOP_TORRENT_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ID): cv.positive_int, - } + +SERVICE_REMOVE_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend( + { + vol.Required(CONF_ID): cv.positive_int, + vol.Optional(ATTR_DELETE_DATA, default=DEFAULT_DELETE_DATA): cv.boolean, + } + ), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), +) + +SERVICE_START_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend({vol.Required(CONF_ID): cv.positive_int}), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), +) + +SERVICE_STOP_TORRENT_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend( + { + vol.Required(CONF_ID): cv.positive_int, + } + ), + cv.has_at_least_one_key(CONF_ENTRY_ID, CONF_NAME), ) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -135,6 +149,39 @@ async def get_api(hass, entry): raise UnknownError from error +def _get_client(hass: HomeAssistant, data: dict[str, Any]) -> TransmissionClient | None: + """Return client from integration name or entry_id.""" + if ( + (entry_id := data.get(CONF_ENTRY_ID)) + and (entry := hass.config_entries.async_get_entry(entry_id)) + and entry.state == ConfigEntryState.LOADED + ): + return hass.data[DOMAIN][entry_id] + + # to be removed once name key is removed + if CONF_NAME in data: + create_issue( + hass, + DOMAIN, + "deprecated_key", + breaks_in_ha_version="2023.1.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_key", + ) + + _LOGGER.warning( + 'The "name" key in the Transmission services is deprecated and will be removed in "2023.1.0"; ' + 'use the "entry_id" key instead to identity which entry to call' + ) + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == data[CONF_NAME]: + return hass.data[DOMAIN][entry.entry_id] + + return None + + class TransmissionClient: """Transmission Client Object.""" @@ -174,14 +221,9 @@ class TransmissionClient: def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" - 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 + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent = service.data[ATTR_TORRENT] if torrent.startswith( ("http", "ftp:", "magnet:") @@ -195,42 +237,27 @@ class TransmissionClient: def start_torrent(service: ServiceCall) -> None: """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 + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent_id = service.data[CONF_ID] tm_client.tm_api.start_torrent(torrent_id) tm_client.api.update() def stop_torrent(service: ServiceCall) -> None: """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 + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent_id = service.data[CONF_ID] tm_client.tm_api.stop_torrent(torrent_id) tm_client.api.update() def remove_torrent(service: ServiceCall) -> None: """Remove 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 + if not (tm_client := _get_client(self.hass, service.data)): + raise ValueError("Transmission instance is not found") + torrent_id = service.data[CONF_ID] delete_data = service.data[ATTR_DELETE_DATA] tm_client.tm_api.remove_torrent(torrent_id, delete_data=delete_data) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 185148f3bd9..742ef874a35 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -18,7 +18,7 @@ SUPPORTED_ORDER_MODES = { torrents, key=lambda t: t.ratio, reverse=True ), } - +CONF_ENTRY_ID = "entry_id" CONF_LIMIT = "limit" CONF_ORDER = "order" diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index 74861df5a70..2fd4793c785 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -2,10 +2,15 @@ add_torrent: name: Add torrent description: Add a new torrent to download (URL, magnet link or Base64 encoded). fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config - required: true example: Transmission selector: text: @@ -21,10 +26,15 @@ remove_torrent: name: Remove torrent description: Remove a torrent fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config - required: true example: Transmission selector: text: @@ -46,6 +56,12 @@ start_torrent: name: Start torrent description: Start a torrent fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config @@ -63,10 +79,15 @@ stop_torrent: name: Stop torrent description: Stop a torrent fields: + entry_id: + name: Transmission entry + description: Config entry id + selector: + config_entry: + integration: transmission name: name: Name description: Instance name as entered during entry config - required: true example: Transmission selector: text: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 0194917c416..a8ba9e5fcb3 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -40,5 +40,18 @@ } } } + }, + "issues": { + "deprecated_key": { + "title": "The name key in Transmission services is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The name key in Transmission services is being removed", + "description": "Update any automations or scripts that use this service and replace the name key with the entry_id key." + } + } + } + } } } diff --git a/homeassistant/components/transmission/translations/bg.json b/homeassistant/components/transmission/translations/bg.json index 23c75c464b7..9461cd6e6a1 100644 --- a/homeassistant/components/transmission/translations/bg.json +++ b/homeassistant/components/transmission/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 235e05bb78a..f982fcc9dd2 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei. S'han de substituir totes les claus o entrades anomenades 'name' per 'entry_id'.", + "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" + } + } + }, + "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index 04274f2c1cb..1d04d0674a7 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/translations/de.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, und ersetze den Namensschl\u00fcssel durch den entry_id Schl\u00fcssel.", + "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" + } + } + }, + "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/el.json b/homeassistant/components/transmission/translations/el.json index 6790e6e351d..e18065b34bf 100644 --- a/homeassistant/components/transmission/translations/el.json +++ b/homeassistant/components/transmission/translations/el.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af entry_id.", + "title": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf Transmission \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } + }, + "title": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf Transmission \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index 3726f6f0a7e..46ab2c64ad9 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service and replace the name key with the entry_id key.", + "title": "The name key in Transmission services is being removed" + } + } + }, + "title": "The name key in Transmission services is being removed" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 30180811cb4..69242bda413 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio y sustituye la clave nombre por la clave entry_id.", + "title": "Se va a eliminar la clave nombre en los servicios de Transmission" + } + } + }, + "title": "Se va a eliminar la clave nombre en los servicios de Transmission" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/et.json b/homeassistant/components/transmission/translations/et.json index 745ef1030af..3fab9d169db 100644 --- a/homeassistant/components/transmission/translations/et.json +++ b/homeassistant/components/transmission/translations/et.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte ja asenda nimev\u00f5ti v\u00f5tmega entry_id-ga.", + "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" + } + } + }, + "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 1bd7129ed6b..b9eae6b7f6f 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, \u00e9s cser\u00e9lje ki a name kulcsot a entry_id kulcsra.", + "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/id.json b/homeassistant/components/transmission/translations/id.json index 7b5fa3a703f..98a5e918740 100644 --- a/homeassistant/components/transmission/translations/id.json +++ b/homeassistant/components/transmission/translations/id.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini dan ganti kunci name dengan kunci entry_id.", + "title": "Kunci name dalam layanan Transmission sedang dihapus" + } + } + }, + "title": "Kunci name dalam layanan Transmission sedang dihapus" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/it.json b/homeassistant/components/transmission/translations/it.json index 2cefcdfb290..41eaa4efdf5 100644 --- a/homeassistant/components/transmission/translations/it.json +++ b/homeassistant/components/transmission/translations/it.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna eventuali automazioni o script che utilizzano questo servizio e sostituisci la chiave del nome con la chiave entry_id.", + "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" + } + } + }, + "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index dfe188f6e3b..2a03ee48b74 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten og erstatt navnen\u00f8kkelen med entry_id-n\u00f8kkelen.", + "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" + } + } + }, + "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index 994744a3547..7ae84ea4f4e 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi i zast\u0105p klucz nazwy kluczem entry_id.", + "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" + } + } + }, + "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index 5579b64e2d9..878e911564d 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o e substitua a chave de nome pela chave entry_id.", + "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" + } + } + }, + "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index ba6787eed7d..cfd1c7e0e84 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 name \u043d\u0430 \u043a\u043b\u044e\u0447 entry_id.", + "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" + } + } + }, + "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/sk.json b/homeassistant/components/transmission/translations/sk.json index 731004b0ebc..178b0043359 100644 --- a/homeassistant/components/transmission/translations/sk.json +++ b/homeassistant/components/transmission/translations/sk.json @@ -1,14 +1,50 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie", "name_exists": "N\u00e1zov u\u017e existuje" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Heslo pre {username} je neplatn\u00e9.", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov", - "port": "Port" + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, a nahra\u010fte k\u013e\u00fa\u010d n\u00e1zvu k\u013e\u00fa\u010dom entry_id.", + "title": "K\u013e\u00fa\u010d s n\u00e1zvom v slu\u017eb\u00e1ch prenosu sa odstra\u0148uje" + } + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frekvencia aktualiz\u00e1cie" } } } diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index fd3d3a909aa..235a13f2c01 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3 name key \u70ba entry_id key\u3002", + "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + } + }, + "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 0e0c41e5e30..5914512a315 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -240,7 +240,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Register the service description service_desc = { - CONF_NAME: f"Say an TTS message with {p_type}", + CONF_NAME: f"Say a TTS message with {p_type}", CONF_DESCRIPTION: f"Say something using text-to-speech on a media player with {p_type}.", CONF_FIELDS: services_dict[SERVICE_SAY][CONF_FIELDS], } diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index aae50902d03..d5122862e2c 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -97,7 +97,6 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): description: AlarmControlPanelEntityDescription, ) -> None: """Init Tuya Alarm.""" - self._attr_supported_features = 0 super().__init__(device, device_manager) self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 757701d5382..20e36028dba 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -25,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import IntegerTypeData, TuyaEntity -from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode, DPType +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType TUYA_HVAC_TO_HA = { "auto": HVACMode.HEAT_COOL, @@ -134,7 +134,6 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ) -> None: """Determine which values to use.""" self._attr_target_temperature_step = 1.0 - self._attr_supported_features = 0 self.entity_description = description super().__init__(device, device_manager) @@ -206,10 +205,19 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): DPCode.MODE, dptype=DPType.ENUM, prefer_function=True ): self._attr_hvac_modes = [HVACMode.OFF] - for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): - if tuya_mode in enum_type.range: + unknown_hvac_modes: list[str] = [] + for tuya_mode in enum_type.range: + if tuya_mode in TUYA_HVAC_TO_HA: + ha_mode = TUYA_HVAC_TO_HA[tuya_mode] self._hvac_to_tuya[ha_mode] = tuya_mode self._attr_hvac_modes.append(ha_mode) + else: + unknown_hvac_modes.append(tuya_mode) + + if unknown_hvac_modes: # Tuya modes are presets instead of hvac_modes + self._attr_hvac_modes.append(description.switch_only_hvac_mode) + self._attr_preset_modes = unknown_hvac_modes + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE elif self.find_dpcode(DPCode.SWITCH, prefer_function=True): self._attr_hvac_modes = [ HVACMode.OFF, @@ -264,18 +272,6 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): """Call when entity is added to hass.""" await super().async_added_to_hass() - # Log unknown modes - if enum_type := self.find_dpcode( - DPCode.MODE, dptype=DPType.ENUM, prefer_function=True - ): - for tuya_mode in enum_type.range: - if tuya_mode not in TUYA_HVAC_TO_HA: - LOGGER.warning( - "Unknown HVAC mode '%s' for device %s; assuming it as off", - tuya_mode, - self.device.name, - ) - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVACMode.OFF}] @@ -285,6 +281,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ) self._send_command(commands) + def set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + commands = [{"code": DPCode.MODE, "value": preset_mode}] + self._send_command(commands) + def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" self._send_command([{"code": DPCode.FAN_SPEED_ENUM, "value": fan_mode}]) @@ -421,8 +422,24 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ) is not None and mode in TUYA_HVAC_TO_HA: return TUYA_HVAC_TO_HA[mode] + # If the switch is on, and the mode does not match any hvac mode. + if self.device.status.get(DPCode.SWITCH, False): + return self.entity_description.switch_only_hvac_mode + return HVACMode.OFF + @property + def preset_mode(self) -> str | None: + """Return preset mode.""" + if DPCode.MODE not in self.device.function: + return None + + mode = self.device.status.get(DPCode.MODE) + if mode in TUYA_HVAC_TO_HA: + return None + + return mode + @property def fan_mode(self) -> str | None: """Return fan mode.""" diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index d8b0a97480e..5bb9c794ca4 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -191,7 +191,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): super().__init__(device, device_manager) self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - self._attr_supported_features = 0 + self._attr_supported_features = CoverEntityFeature(0) # Check if this cover is based on a switch or has controls if self.find_dpcode(description.key, prefer_function=True): diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 9891c81a456..765de4d860a 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -93,7 +93,6 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): super().__init__(device, device_manager) self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - self._attr_supported_features = 0 # Determine main switch DPCode self._switch_dpcode = self.find_dpcode( diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 9b78008af55..51517007f71 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -123,6 +123,10 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { color_temp=DPCode.TEMP_VALUE, color_data=DPCode.COLOUR_DATA, ), + # Some ceiling fan lights use LIGHT for DPCode instead of SWITCH_LED + TuyaLightEntityDescription( + key=DPCode.LIGHT, + ), ), # Ambient Light # https://developer.tuya.com/en/docs/iot/ambient-light?id=Kaiuz06amhe6g diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 39874d0ae8d..21c6dcee6e1 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -410,7 +410,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): return None # Raw value - if not (value := self.device.status.get(self.entity_description.key)): + if (value := self.device.status.get(self.entity_description.key)) is None: return None return self._number.scale_value(value) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index e19bcd20ba5..98969fe4c48 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -90,7 +90,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { key=DPCode.GAS_SENSOR_VALUE, name="Gas", icon="mdi:gas-cylinder", - device_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH4_SENSOR_VALUE, @@ -157,7 +157,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -205,6 +205,23 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # Two-way temperature and humidity switch + # "MOES Temperature and Humidity Smart Switch Module MS-103" + # Documentation not found + "wkcz": ( + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ), # CO Detector # https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v "cobj": ( @@ -497,7 +514,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, icon="mdi:gas-cylinder", - device_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), @@ -631,7 +648,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, ), diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 3ec24f4daab..329b170c53f 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -142,6 +142,21 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { icon="mdi:power-sleep", ), ), + # Two-way temperature and humidity switch + # "MOES Temperature and Humidity Smart Switch Module MS-103" + # Documentation not found + "wkcz": ( + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Switch 1", + device_class=SwitchDeviceClass.OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_2, + name="Switch 2", + device_class=SwitchDeviceClass.OUTLET, + ), + ), # Switch # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "kg": ( diff --git a/homeassistant/components/tuya/translations/select.sk.json b/homeassistant/components/tuya/translations/select.sk.json index 493576472b9..94f4d8ec464 100644 --- a/homeassistant/components/tuya/translations/select.sk.json +++ b/homeassistant/components/tuya/translations/select.sk.json @@ -1,16 +1,66 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Zak\u00e1zan\u00e9", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "1": "Neakt\u00edvny", + "2": "Akt\u00edvny" + }, "tuya__countdown": { - "3h": "3 hodiny" + "1h": "1 hodina", + "2h": "2 hodiny", + "3h": "3 hodiny", + "4h": "4 hodiny", + "5h": "5 hod\u00edn", + "6h": "6 hod\u00edn", + "cancel": "Zru\u0161i\u0165" + }, + "tuya__curtain_mode": { + "morning": "R\u00e1no", + "night": "Noc" + }, + "tuya__curtain_motor_mode": { + "back": "Sp\u00e4\u0165", + "forward": "Dopredu" }, "tuya__decibel_sensitivity": { "1": "Vysok\u00e1 citlivos\u0165" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, + "tuya__fingerbot_mode": { + "switch": "Prep\u00edna\u010d" + }, + "tuya__humidifier_spray_mode": { + "humidity": "Vlhkos\u0165" + }, + "tuya__led_type": { + "halogen": "Halog\u00e9n", + "incandescent": "\u017diarovka", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Neakt\u00edvny" + }, "tuya__motion_sensitivity": { "0": "N\u00edzka citlivos\u0165", "1": "Stredn\u00e1 citlivos\u0165", "2": "Vysok\u00e1 citlivos\u0165" }, + "tuya__relay_status": { + "last": "Zapam\u00e4ta\u0165 posledn\u00fd stav", + "memory": "Zapam\u00e4ta\u0165 posledn\u00fd stav", + "off": "Neakt\u00edvny", + "on": "Akt\u00edvny", + "power_off": "Neakt\u00edvny", + "power_on": "Akt\u00edvny" + }, "tuya__vacuum_mode": { "random": "N\u00e1hodn\u00fd" } diff --git a/homeassistant/components/tuya/translations/sensor.sk.json b/homeassistant/components/tuya/translations/sensor.sk.json index 4f80ab106ad..c1b9162966d 100644 --- a/homeassistant/components/tuya/translations/sensor.sk.json +++ b/homeassistant/components/tuya/translations/sensor.sk.json @@ -1,7 +1,13 @@ { "state": { "tuya__status": { - "boiling_temp": "Teplota varu" + "boiling_temp": "Teplota varu", + "cooling": "Chladenie", + "heating": "Vykurovanie", + "heating_temp": "Teplota vykurovania", + "reserve_1": "Rezerva 1", + "reserve_2": "Rezerva 2", + "reserve_3": "Rezerva 3" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sk.json b/homeassistant/components/tuya/translations/sk.json index 93fa886f796..267c2bef955 100644 --- a/homeassistant/components/tuya/translations/sk.json +++ b/homeassistant/components/tuya/translations/sk.json @@ -1,12 +1,14 @@ { "config": { "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "login_error": "Chyba pri prihlasovan\u00ed ({code}): {msg}" }, "step": { "user": { "data": { - "country_code": "Krajina" + "country_code": "Krajina", + "password": "Heslo" } } } diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index d17452b11c6..27fe764b1e3 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -83,7 +83,6 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): """Init Tuya vacuum.""" super().__init__(device, device_manager) - self._attr_supported_features = 0 self._attr_fan_speed_list = [] self._attr_supported_features |= VacuumEntityFeature.SEND_COMMAND diff --git a/homeassistant/components/twentemilieu/translations/sk.json b/homeassistant/components/twentemilieu/translations/sk.json new file mode 100644 index 00000000000..41a4042d8b3 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_address": "Adresa sa nena\u0161la v servisnej oblasti Twente Milieu." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/hr.json b/homeassistant/components/twilio/translations/hr.json new file mode 100644 index 00000000000..5307b9f4eb0 --- /dev/null +++ b/homeassistant/components/twilio/translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u017delite li zapo\u010deti s postavljanjem?", + "title": "Postavite Twilio Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/sk.json b/homeassistant/components/twilio/translations/sk.json new file mode 100644 index 00000000000..04cb32a1c4e --- /dev/null +++ b/homeassistant/components/twilio/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Nie je pripojen\u00e9 k Home Assistant Cloud.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/const.py b/homeassistant/components/twinkly/const.py index e48ff165c67..d0d905a5752 100644 --- a/homeassistant/components/twinkly/const.py +++ b/homeassistant/components/twinkly/const.py @@ -9,6 +9,7 @@ CONF_NAME = "name" # Strongly named HA attributes keys ATTR_HOST = "host" +ATTR_VERSION = "version" # Keys of attributes read from the get_device_info DEV_ID = "uuid" @@ -27,3 +28,6 @@ HIDDEN_DEV_VALUES = ( "copyright", # We should not display a copyright "LEDWORKS 2018" in the Home-Assistant UI "mac", # Does not report the actual device mac address ) + +# Minimum version required to support effects +MIN_EFFECT_VERSION = "2.7.1" diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index ba6ac7cf492..3174f60edf8 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -2,18 +2,22 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging from typing import Any from aiohttp import ClientError +from awesomeversion import AwesomeVersion from ttls.client import Twinkly from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ColorMode, LightEntity, + LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL @@ -22,6 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + ATTR_VERSION, CONF_HOST, CONF_ID, CONF_NAME, @@ -34,6 +39,7 @@ from .const import ( DEV_PROFILE_RGBW, DOMAIN, HIDDEN_DEV_VALUES, + MIN_EFFECT_VERSION, ) _LOGGER = logging.getLogger(__name__) @@ -91,6 +97,11 @@ class TwinklyLight(LightEntity): self._is_on = False self._is_available = False self._attributes: dict[Any, Any] = {} + self._current_movie: dict[Any, Any] = {} + self._movies: list[Any] = [] + self._software_version = "" + # We guess that most devices are "new" and support effects + self._attr_supported_features = LightEntityFeature.EFFECT @property def available(self) -> bool: @@ -125,6 +136,7 @@ class TwinklyLight(LightEntity): manufacturer="LEDWORKS", model=self.model, name=self.name, + sw_version=self._software_version, ) @property @@ -133,13 +145,41 @@ class TwinklyLight(LightEntity): return self._is_on @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> Mapping[str, Any]: """Return device specific state attributes.""" attributes = self._attributes return attributes + @property + def effect(self) -> str | None: + """Return the current effect.""" + if "name" in self._current_movie: + return f"{self._current_movie['id']} {self._current_movie['name']}" + return None + + @property + def effect_list(self) -> list[str]: + """Return the list of saved effects.""" + effect_list = [] + for movie in self._movies: + effect_list.append(f"{movie['id']} {movie['name']}") + return effect_list + + async def async_added_to_hass(self) -> None: + """Device is added to hass.""" + software_version = await self._client.get_firmware_version() + if ATTR_VERSION in software_version: + self._software_version = software_version[ATTR_VERSION] + + if AwesomeVersion(self._software_version) < AwesomeVersion( + MIN_EFFECT_VERSION + ): + self._attr_supported_features = ( + self.supported_features & ~LightEntityFeature.EFFECT + ) + async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" if ATTR_BRIGHTNESS in kwargs: @@ -153,33 +193,62 @@ class TwinklyLight(LightEntity): await self._client.set_brightness(brightness) - if ATTR_RGBW_COLOR in kwargs: - if kwargs[ATTR_RGBW_COLOR] != self._attr_rgbw_color: - self._attr_rgbw_color = kwargs[ATTR_RGBW_COLOR] + if ( + ATTR_RGBW_COLOR in kwargs + and kwargs[ATTR_RGBW_COLOR] != self._attr_rgbw_color + ): - if isinstance(self._attr_rgbw_color, tuple): - - await self._client.interview() - # Reagarrange from rgbw to wrgb - await self._client.set_static_colour( - ( - self._attr_rgbw_color[3], - self._attr_rgbw_color[0], - self._attr_rgbw_color[1], - self._attr_rgbw_color[2], - ) + await self._client.interview() + if LightEntityFeature.EFFECT & self.supported_features: + # Static color only supports rgb + await self._client.set_static_colour( + ( + kwargs[ATTR_RGBW_COLOR][0], + kwargs[ATTR_RGBW_COLOR][1], + kwargs[ATTR_RGBW_COLOR][2], ) + ) + await self._client.set_mode("color") + self._client.default_mode = "color" + else: + await self._client.set_cycle_colours( + ( + kwargs[ATTR_RGBW_COLOR][3], + kwargs[ATTR_RGBW_COLOR][0], + kwargs[ATTR_RGBW_COLOR][1], + kwargs[ATTR_RGBW_COLOR][2], + ) + ) + await self._client.set_mode("movie") + self._client.default_mode = "movie" + self._attr_rgbw_color = kwargs[ATTR_RGBW_COLOR] - if ATTR_RGB_COLOR in kwargs: - if kwargs[ATTR_RGB_COLOR] != self._attr_rgb_color: - self._attr_rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_RGB_COLOR in kwargs and kwargs[ATTR_RGB_COLOR] != self._attr_rgb_color: - if isinstance(self._attr_rgb_color, tuple): + await self._client.interview() + if LightEntityFeature.EFFECT & self.supported_features: + await self._client.set_static_colour(kwargs[ATTR_RGB_COLOR]) + await self._client.set_mode("color") + self._client.default_mode = "color" + else: + await self._client.set_cycle_colours(kwargs[ATTR_RGB_COLOR]) + await self._client.set_mode("movie") + self._client.default_mode = "movie" - await self._client.interview() - # Reagarrange from rgbw to wrgb - await self._client.set_static_colour(self._attr_rgb_color) + self._attr_rgb_color = kwargs[ATTR_RGB_COLOR] + if ( + ATTR_EFFECT in kwargs + and LightEntityFeature.EFFECT & self.supported_features + ): + movie_id = kwargs[ATTR_EFFECT].split(" ")[0] + if "id" not in self._current_movie or int(movie_id) != int( + self._current_movie["id"] + ): + await self._client.interview() + await self._client.set_current_movie(int(movie_id)) + await self._client.set_mode("movie") + self._client.default_mode = "movie" if not self._is_on: await self._client.turn_on() @@ -232,6 +301,10 @@ class TwinklyLight(LightEntity): if key not in HIDDEN_DEV_VALUES: self._attributes[key] = value + if LightEntityFeature.EFFECT & self.supported_features: + await self.async_update_movies() + await self.async_update_current_movie() + if not self._is_available: _LOGGER.info("Twinkly '%s' is now available", self._client.host) @@ -245,3 +318,17 @@ class TwinklyLight(LightEntity): "Twinkly '%s' is not reachable (client error)", self._client.host ) self._is_available = False + + async def async_update_movies(self) -> None: + """Update the list of movies (effects).""" + movies = await self._client.get_saved_movies() + _LOGGER.debug("Movies: %s", movies) + if movies and "movies" in movies: + self._movies = movies["movies"] + + async def async_update_current_movie(self) -> None: + """Update the current active movie.""" + current_movie = await self._client.get_current_movie() + _LOGGER.debug("Current movie: %s", current_movie) + if current_movie and "id" in current_movie: + self._current_movie = current_movie diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index e3b97e9385b..b41d9bc1d0a 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -2,7 +2,7 @@ "domain": "twinkly", "name": "Twinkly", "documentation": "https://www.home-assistant.io/integrations/twinkly", - "requirements": ["ttls==1.4.3"], + "requirements": ["ttls==1.5.1"], "codeowners": ["@dr1rrb", "@Robbie1221"], "config_flow": true, "dhcp": [{ "hostname": "twinkly_*" }], diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index 5d096c5b594..6512565c072 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -8,7 +8,7 @@ }, "step": { "discovery_confirm": { - "description": "M\u00f6chtest du {name} - {model} ( {host} ) einrichten?" + "description": "M\u00f6chtest du {name} - {model} ({host}) einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/twinkly/translations/sk.json b/homeassistant/components/twinkly/translations/sk.json new file mode 100644 index 00000000000..297d67d205d --- /dev/null +++ b/homeassistant/components/twinkly/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "device_exists": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} - {model} ({host})?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ubiwizz/__init__.py b/homeassistant/components/ubiwizz/__init__.py new file mode 100644 index 00000000000..0126a15b983 --- /dev/null +++ b/homeassistant/components/ubiwizz/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Ubiwizz.""" diff --git a/homeassistant/components/ubiwizz/manifest.json b/homeassistant/components/ubiwizz/manifest.json new file mode 100644 index 00000000000..a6b5d6e7317 --- /dev/null +++ b/homeassistant/components/ubiwizz/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "ubiwizz", + "name": "Ubiwizz", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index 4dbbb1d5964..7fc727cf9fe 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -82,6 +82,7 @@ def setup_platform( class UERadioDevice(MediaPlayerEntity): """Representation of a Logitech UE Smart Radio device.""" + _attr_icon = ICON _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PLAY @@ -99,13 +100,9 @@ class UERadioDevice(MediaPlayerEntity): """Initialize the Logitech UE Smart Radio device.""" self._session = session self._player_id = player_id - self._name = player_name - self._state = None - self._volume = 0 + self._attr_name = player_name + self._attr_volume_level = 0 self._last_volume = 0 - self._media_title = None - self._media_artist = None - self._media_artwork_url = None def send_command(self, command): """Send command to radio.""" @@ -128,63 +125,28 @@ class UERadioDevice(MediaPlayerEntity): ) if request["error"] is not None: - self._state = None + self._attr_state = None return if request["result"]["power"] == 0: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF else: - self._state = PLAYBACK_DICT[request["result"]["mode"]] + self._attr_state = PLAYBACK_DICT[request["result"]["mode"]] media_info = request["result"]["playlist_loop"][0] - self._volume = request["result"]["mixer volume"] / 100 - self._media_artwork_url = media_info["artwork_url"] - self._media_title = media_info["title"] + self._attr_volume_level = request["result"]["mixer volume"] / 100 + self._attr_media_image_url = media_info["artwork_url"] + self._attr_media_title = media_info["title"] if "artist" in media_info: - self._media_artist = media_info["artist"] + self._attr_media_artist = media_info["artist"] else: - self._media_artist = media_info.get("remote_title") + self._attr_media_artist = media_info.get("remote_title") @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - - @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool: """Boolean if volume is currently muted.""" - return self._volume <= 0 - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def media_image_url(self): - """Image URL of current playing media.""" - return self._media_artwork_url - - @property - def media_artist(self): - """Artist of current playing media, music track only.""" - return self._media_artist - - @property - def media_title(self): - """Title of current playing media.""" - return self._media_title + return self.volume_level is not None and self.volume_level <= 0 def turn_on(self) -> None: """Turn on specified media player or all.""" @@ -217,7 +179,7 @@ class UERadioDevice(MediaPlayerEntity): def mute_volume(self, mute: bool) -> None: """Send mute command.""" if mute: - self._last_volume = self._volume + self._last_volume = self.volume_level self.send_command(["mixer", "volume", 0]) else: self.send_command(["mixer", "volume", self._last_volume * 100]) diff --git a/homeassistant/components/ukraine_alarm/translations/cs.json b/homeassistant/components/ukraine_alarm/translations/cs.json index 5073c9248e0..e45d8f4ef7e 100644 --- a/homeassistant/components/ukraine_alarm/translations/cs.json +++ b/homeassistant/components/ukraine_alarm/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } diff --git a/homeassistant/components/ukraine_alarm/translations/sk.json b/homeassistant/components/ukraine_alarm/translations/sk.json new file mode 100644 index 00000000000..04a10161b8b --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/sk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "timeout": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "community": { + "data": { + "region": "Regi\u00f3n" + } + }, + "district": { + "data": { + "region": "Regi\u00f3n" + } + }, + "user": { + "data": { + "region": "Regi\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 84540f7bea4..adaa7c977f7 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,20 +1,14 @@ """Integration to UniFi Network and its various features.""" -from collections.abc import Mapping -from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_CONTROLLER, - DOMAIN as UNIFI_DOMAIN, - PLATFORMS, - UNIFI_WIRELESS_CLIENTS, -) +from .const import DOMAIN as UNIFI_DOMAIN, PLATFORMS, UNIFI_WIRELESS_CLIENTS from .controller import UniFiController, get_unifi_controller from .errors import AuthenticationRequired, CannotConnect from .services import async_setup_services, async_unload_services @@ -36,8 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up the UniFi Network integration.""" hass.data.setdefault(UNIFI_DOMAIN, {}) - # Flat configuration was introduced with 2021.3 - await async_flatten_entry_data(hass, config_entry) + # Removal of legacy PoE control was introduced with 2022.12 + async_remove_poe_client_entities(hass, config_entry) try: api = await get_unifi_controller(hass, config_entry.data) @@ -50,12 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err - # Unique ID was introduced with 2021.3 - if config_entry.unique_id is None: - hass.config_entries.async_update_entry( - config_entry, unique_id=controller.site_id - ) - hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await controller.async_update_device_registry() @@ -82,20 +70,22 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return await controller.async_reset() -async def async_flatten_entry_data( +@callback +def async_remove_poe_client_entities( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: - """Simpler configuration structure for entry data. + """Remove PoE client entities.""" + ent_reg = er.async_get(hass) - Keep controller key layer in case user rollbacks. - """ + entity_ids_to_be_removed = [ + entry.entity_id + for entry in ent_reg.entities.values() + if entry.config_entry_id == config_entry.entry_id + and entry.unique_id.startswith("poe-") + ] - data: Mapping[str, Any] = { - **config_entry.data, - **config_entry.data[CONF_CONTROLLER], - } - if config_entry.data != data: - hass.config_entries.async_update_entry(config_entry, data=data) + for entity_id in entity_ids_to_be_removed: + ent_reg.async_remove(entity_id) class UnifiWirelessClients: @@ -104,30 +94,30 @@ class UnifiWirelessClients: This is needed since wireless devices going offline might get marked as wired by UniFi. """ - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Set up client storage.""" self.hass = hass - self.data = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self.data: dict[str, dict[str, list[str]]] = {} + self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - async def async_load(self): + async def async_load(self) -> None: """Load data from file.""" if (data := await self._store.async_load()) is not None: self.data = data @callback - def get_data(self, config_entry): + def get_data(self, config_entry: ConfigEntry) -> set[str]: """Get data related to a specific controller.""" data = self.data.get(config_entry.entry_id, {"wireless_devices": []}) return set(data["wireless_devices"]) @callback - def update_data(self, data, config_entry): + def update_data(self, data: set[str], config_entry: ConfigEntry) -> None: """Update data and schedule to save to file.""" self.data[config_entry.entry_id] = {"wireless_devices": list(data)} self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> dict[str, dict[str, list[str]]]: """Return data of UniFi wireless clients to store in a file.""" return self.data diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 4944dd91296..caf256ded20 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -37,14 +37,12 @@ from .const import ( CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, - CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, DEFAULT_DPI_RESTRICTIONS, - DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) from .controller import UniFiController, get_unifi_controller @@ -396,10 +394,6 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_BLOCK_CLIENT, default=selected_clients_to_block ): cv.multi_select(clients_to_block), - vol.Optional( - CONF_POE_CLIENTS, - default=self.options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS), - ): bool, vol.Optional( CONF_DPI_RESTRICTIONS, default=self.options.get( diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index bf0aaef45dd..85f744e481f 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -25,7 +25,6 @@ CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" CONF_DPI_RESTRICTIONS = "dpi_restrictions" CONF_IGNORE_WIRED_BUG = "ignore_wired_bug" -CONF_POE_CLIENTS = "poe_clients" CONF_TRACK_CLIENTS = "track_clients" CONF_TRACK_DEVICES = "track_devices" CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" @@ -35,7 +34,6 @@ DEFAULT_ALLOW_BANDWIDTH_SENSORS = False DEFAULT_ALLOW_UPTIME_SENSORS = False DEFAULT_DPI_RESTRICTIONS = True DEFAULT_IGNORE_WIRED_BUG = False -DEFAULT_POE_CLIENTS = True DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True DEFAULT_TRACK_WIRED_CLIENTS = True @@ -45,5 +43,4 @@ ATTR_MANUFACTURER = "Ubiquiti Networks" BLOCK_SWITCH = "block" DPI_SWITCH = "dpi" -POE_SWITCH = "poe" OUTLET_SWITCH = "outlet" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index c421cb5391a..8aae95bda41 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -44,7 +44,6 @@ from .const import ( CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, - CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, @@ -55,14 +54,12 @@ from .const import ( DEFAULT_DETECTION_TIME, DEFAULT_DPI_RESTRICTIONS, DEFAULT_IGNORE_WIRED_BUG, - DEFAULT_POE_CLIENTS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, LOGGER, PLATFORMS, - POE_SWITCH, UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect @@ -140,8 +137,6 @@ class UniFiController: # 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. @@ -305,9 +300,8 @@ class UniFiController: ): if entry.domain == Platform.DEVICE_TRACKER: mac = entry.unique_id.split("-", 1)[0] - elif entry.domain == Platform.SWITCH and ( - entry.unique_id.startswith(BLOCK_SWITCH) - or entry.unique_id.startswith(POE_SWITCH) + elif entry.domain == Platform.SWITCH and entry.unique_id.startswith( + BLOCK_SWITCH ): mac = entry.unique_id.split("-", 1)[1] else: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 1c91ea4724b..ea8db77e124 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -6,8 +6,7 @@ import logging from aiounifi.models.api import SOURCE_DATA, SOURCE_EVENT from aiounifi.models.event import EventKey -from homeassistant.components.device_tracker import DOMAIN, SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import DOMAIN, ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index b35fd520ab0..495613f3b81 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -95,7 +95,6 @@ async def async_get_config_entry_diagnostics( async_replace_dict_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG ) diag["site_role"] = controller.site_role - diag["entities"] = async_replace_dict_data(controller.entities, macs_to_redact) diag["clients"] = { macs_to_redact[k]: async_redact_data( async_replace_dict_data(v.raw, macs_to_redact), REDACT_CLIENTS diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 5b96560f8c5..c9e9464a317 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==41"], + "requirements": ["aiounifi==42"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 43a08b58eac..3e98f41188a 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -7,24 +7,33 @@ Support for controlling deep packet inspection (DPI) restriction groups. from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any, Generic, TypeVar -from aiounifi.interfaces.api_handlers import ItemEvent +import aiounifi +from aiounifi.interfaces.api_handlers import CallbackType, ItemEvent, UnsubscribeType from aiounifi.interfaces.clients import Clients from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.ports import Ports -from aiounifi.models.client import ClientBlockRequest +from aiounifi.models.client import Client, ClientBlockRequest from aiounifi.models.device import ( DeviceSetOutletRelayRequest, DeviceSetPoePortModeRequest, ) from aiounifi.models.dpi_restriction_app import DPIRestrictionAppEnableRequest +from aiounifi.models.dpi_restriction_group import DPIRestrictionGroup from aiounifi.models.event import Event, EventKey +from aiounifi.models.outlet import Outlet +from aiounifi.models.port import Port -from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity +from homeassistant.components.switch import ( + DOMAIN, + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -35,35 +44,221 @@ from homeassistant.helpers.device_registry import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity -from .const import ( - ATTR_MANUFACTURER, - BLOCK_SWITCH, - DOMAIN as UNIFI_DOMAIN, - DPI_SWITCH, - OUTLET_SWITCH, - POE_SWITCH, -) +from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .controller import UniFiController -from .unifi_client import UniFiClient CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED) -T = TypeVar("T") +Data = TypeVar("Data") +Handler = TypeVar("Handler") + +Subscription = Callable[[CallbackType, ItemEvent], UnsubscribeType] + + +@callback +def async_dpi_group_is_on_fn( + api: aiounifi.Controller, dpi_group: DPIRestrictionGroup +) -> bool: + """Calculate if all apps are enabled.""" + return all( + api.dpi_apps[app_id].enabled + for app_id in dpi_group.dpiapp_ids or [] + if app_id in api.dpi_apps + ) + + +@callback +def async_sub_device_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if sub device object is disabled.""" + device_id = obj_id.partition("_")[0] + device = controller.api.devices[device_id] + return controller.available and not device.disabled + + +@callback +def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for client.""" + client = api.clients[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, obj_id)}, + default_manufacturer=client.oui, + default_name=client.name or client.hostname, + ) + + +@callback +def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for device.""" + if "_" in obj_id: # Sub device + obj_id = obj_id.partition("_")[0] + + device = api.devices[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, device.mac)}, + manufacturer=ATTR_MANUFACTURER, + model=device.model, + name=device.name or None, + sw_version=device.version, + hw_version=str(device.board_revision), + ) + + +@callback +def async_dpi_group_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for DPI group.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, f"unifi_controller_{obj_id}")}, + manufacturer=ATTR_MANUFACTURER, + model="UniFi Network", + name="UniFi Network", + ) + + +async def async_block_client_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control network access of client.""" + await api.request(ClientBlockRequest.create(obj_id, not target)) + + +async def async_dpi_group_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Enable or disable DPI group.""" + dpi_group = api.dpi_groups[obj_id] + await asyncio.gather( + *[ + api.request(DPIRestrictionAppEnableRequest.create(app_id, target)) + for app_id in dpi_group.dpiapp_ids or [] + ] + ) + + +async def async_outlet_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control outlet relay.""" + mac, _, index = obj_id.partition("_") + device = api.devices[mac] + await api.request(DeviceSetOutletRelayRequest.create(device, int(index), target)) + + +async def async_poe_port_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control poe state.""" + mac, _, index = obj_id.partition("_") + device = api.devices[mac] + state = "auto" if target else "off" + await api.request(DeviceSetPoePortModeRequest.create(device, int(index), state)) @dataclass -class UnifiEntityLoader(Generic[T]): +class UnifiEntityLoader(Generic[Handler, Data]): """Validate and load entities from different UniFi handlers.""" allowed_fn: Callable[[UniFiController, str], bool] - entity_cls: type[UnifiBlockClientSwitch] | type[UnifiDPIRestrictionSwitch] | type[ - UnifiOutletSwitch - ] | type[UnifiPoePortSwitch] | type[UnifiDPIRestrictionSwitch] - handler_fn: Callable[[UniFiController], T] - supported_fn: Callable[[T, str], bool | None] + api_handler_fn: Callable[[aiounifi.Controller], Handler] + available_fn: Callable[[UniFiController, str], bool] + control_fn: Callable[[aiounifi.Controller, str, bool], Coroutine[Any, Any, None]] + device_info_fn: Callable[[aiounifi.Controller, str], DeviceInfo] + event_is_on: tuple[EventKey, ...] | None + event_to_subscribe: tuple[EventKey, ...] | None + is_on_fn: Callable[[aiounifi.Controller, Data], bool] + name_fn: Callable[[Data], str | None] + object_fn: Callable[[aiounifi.Controller, str], Data] + supported_fn: Callable[[aiounifi.Controller, str], bool | None] + unique_id_fn: Callable[[str], str] + + +@dataclass +class UnifiEntityDescription(SwitchEntityDescription, UnifiEntityLoader[Handler, Data]): + """Class describing UniFi switch entity.""" + + custom_subscribe: Callable[[aiounifi.Controller], Subscription] | None = None + only_event_for_state_change: bool = False + + +ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( + UnifiEntityDescription[Clients, Client]( + key="Block client", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + icon="mdi:ethernet", + allowed_fn=lambda controller, obj_id: obj_id in controller.option_block_clients, + api_handler_fn=lambda api: api.clients, + available_fn=lambda controller, obj_id: controller.available, + control_fn=async_block_client_control_fn, + device_info_fn=async_client_device_info_fn, + event_is_on=CLIENT_UNBLOCKED, + event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED, + is_on_fn=lambda api, client: not client.blocked, + name_fn=lambda client: None, + object_fn=lambda api, obj_id: api.clients[obj_id], + only_event_for_state_change=True, + supported_fn=lambda api, obj_id: True, + unique_id_fn=lambda obj_id: f"block-{obj_id}", + ), + UnifiEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup]( + key="DPI restriction", + entity_category=EntityCategory.CONFIG, + icon="mdi:network", + allowed_fn=lambda controller, obj_id: controller.option_dpi_restrictions, + api_handler_fn=lambda api: api.dpi_groups, + available_fn=lambda controller, obj_id: controller.available, + control_fn=async_dpi_group_control_fn, + custom_subscribe=lambda api: api.dpi_apps.subscribe, + device_info_fn=async_dpi_group_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=async_dpi_group_is_on_fn, + name_fn=lambda group: group.name, + object_fn=lambda api, obj_id: api.dpi_groups[obj_id], + supported_fn=lambda api, obj_id: bool(api.dpi_groups[obj_id].dpiapp_ids), + unique_id_fn=lambda obj_id: obj_id, + ), + UnifiEntityDescription[Outlets, Outlet]( + key="Outlet control", + device_class=SwitchDeviceClass.OUTLET, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.outlets, + available_fn=async_sub_device_available_fn, + control_fn=async_outlet_control_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=lambda api, outlet: outlet.relay_state, + name_fn=lambda outlet: outlet.name, + object_fn=lambda api, obj_id: api.outlets[obj_id], + supported_fn=lambda api, obj_id: api.outlets[obj_id].has_relay, + unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", + ), + UnifiEntityDescription[Ports, Port]( + key="PoE port control", + device_class=SwitchDeviceClass.OUTLET, + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + entity_registry_enabled_default=False, + icon="mdi:ethernet", + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.ports, + available_fn=async_sub_device_available_fn, + control_fn=async_poe_port_control_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=lambda api, port: port.poe_mode != "off", + name_fn=lambda port: f"{port.name} PoE", + object_fn=lambda api, obj_id: api.ports[obj_id], + supported_fn=lambda api, obj_id: api.ports[obj_id].port_poe, + unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", + ), +) async def async_setup_entry( @@ -71,74 +266,32 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up switches for UniFi Network integration. - - Switches are controlling network access and switch ports with POE. - """ + """Set up switches for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - controller.entities[DOMAIN] = { - BLOCK_SWITCH: set(), - POE_SWITCH: set(), - DPI_SWITCH: set(), - OUTLET_SWITCH: set(), - } if controller.site_role != "admin": return - # Store previously known POE control entities in case their POE are turned off. - known_poe_clients = [] - entity_registry = er.async_get(hass) - for entry in er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ): - - if not entry.unique_id.startswith(POE_SWITCH): - continue - - mac = entry.unique_id.replace(f"{POE_SWITCH}-", "") - if mac not in controller.api.clients: - continue - - known_poe_clients.append(mac) - for mac in controller.option_block_clients: if mac not in controller.api.clients and mac in controller.api.clients_all: client = controller.api.clients_all[mac] controller.api.clients.process_raw([client.raw]) @callback - def items_added( - clients: set = controller.api.clients, - devices: set = controller.api.devices, - ) -> None: - """Update the values of the controller.""" - if controller.option_poe_clients: - add_poe_entities(controller, async_add_entities, clients, known_poe_clients) - - for signal in (controller.signal_update, controller.signal_options_update): - config_entry.async_on_unload( - async_dispatcher_connect(hass, signal, items_added) - ) - - items_added() - known_poe_clients.clear() - - @callback - def async_load_entities(loader: UnifiEntityLoader) -> None: + def async_load_entities(description: UnifiEntityDescription) -> None: """Load and subscribe to UniFi devices.""" entities: list[SwitchEntity] = [] - api_handler = loader.handler_fn(controller) + api_handler = description.api_handler_fn(controller.api) @callback def async_create_entity(event: ItemEvent, obj_id: str) -> None: """Create UniFi entity.""" - if not loader.allowed_fn(controller, obj_id) or not loader.supported_fn( - api_handler, obj_id - ): + if not description.allowed_fn( + controller, obj_id + ) or not description.supported_fn(controller.api, obj_id): return - entity = loader.entity_cls(obj_id, controller) + entity = UnifiSwitchEntity(obj_id, controller, description) if event == ItemEvent.ADDED: async_add_entities([entity]) return @@ -150,202 +303,56 @@ async def async_setup_entry( api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - for unifi_loader in UNIFI_LOADERS: - async_load_entities(unifi_loader) + for description in ENTITY_DESCRIPTIONS: + async_load_entities(description) -@callback -def add_poe_entities(controller, async_add_entities, clients, known_poe_clients): - """Add new switch entities from the controller.""" - switches = [] +class UnifiSwitchEntity(SwitchEntity): + """Base representation of a UniFi switch.""" - devices = controller.api.devices - - for mac in clients: - if mac in controller.entities[DOMAIN][POE_SWITCH]: - continue - - client = controller.api.clients[mac] - - # Try to identify new clients powered by POE. - # Known POE clients have been created in previous HASS sessions. - # If port_poe is None the port does not support POE - # If poe_enable is False we can't know if a POE client is available for control. - if mac not in known_poe_clients and ( - mac in controller.wireless_clients - or client.switch_mac not in devices - or not devices[client.switch_mac].ports[client.switch_port].port_poe - or not devices[client.switch_mac].ports[client.switch_port].poe_enable - or controller.mac == client.mac - ): - continue - - # Multiple POE-devices on same port means non UniFi POE driven switch - multi_clients_on_port = False - for client2 in controller.api.clients.values(): - - if mac in known_poe_clients: - break - - if ( - client2.is_wired - and client.mac != client2.mac - and client.switch_mac == client2.switch_mac - and client.switch_port == client2.switch_port - ): - multi_clients_on_port = True - break - - if multi_clients_on_port: - continue - - switches.append(UniFiPOEClientSwitch(client, controller)) - - async_add_entities(switches) - - -class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): - """Representation of a client that uses POE.""" - - DOMAIN = DOMAIN - TYPE = POE_SWITCH - - _attr_entity_category = EntityCategory.CONFIG - - def __init__(self, client, controller): - """Set up POE switch.""" - super().__init__(client, controller) - - self.poe_mode = None - if client.switch_port and self.port.poe_mode != "off": - self.poe_mode = self.port.poe_mode - - async def async_added_to_hass(self) -> None: - """Call when entity about to be added to Home Assistant.""" - await super().async_added_to_hass() - - if self.poe_mode: # POE is enabled and client in a known state - return - - if (state := await self.async_get_last_state()) is None: - return - - self.poe_mode = state.attributes.get("poe_mode") - - if not self.client.switch_mac: - self.client.raw["sw_mac"] = state.attributes.get("switch") - - if not self.client.switch_port: - self.client.raw["sw_port"] = state.attributes.get("port") - - @property - def is_on(self): - """Return true if POE is active.""" - return self.port.poe_mode != "off" - - @property - def available(self) -> bool: - """Return if switch is available. - - Poe_mode None means its POE state is unknown. - Sw_mac unavailable means restored client. - """ - return ( - self.poe_mode is not None - and self.controller.available - and self.client.switch_port - and self.client.switch_mac - and self.client.switch_mac in self.controller.api.devices - ) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Enable POE for client.""" - await self.controller.api.request( - DeviceSetPoePortModeRequest.create( - self.device, self.client.switch_port, self.poe_mode - ) - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Disable POE for client.""" - await self.controller.api.request( - DeviceSetPoePortModeRequest.create( - self.device, self.client.switch_port, "off" - ) - ) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributes = { - "power": self.port.poe_power, - "switch": self.client.switch_mac, - "port": self.client.switch_port, - "poe_mode": self.poe_mode, - } - return attributes - - @property - def device(self): - """Shortcut to the switch that client is connected to.""" - return self.controller.api.devices[self.client.switch_mac] - - @property - def port(self): - """Shortcut to the switch port that client is connected to.""" - return self.device.ports[self.client.switch_port] - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.controller.option_poe_clients: - await self.remove_item({self.client.mac}) - - -class UnifiBlockClientSwitch(SwitchEntity): - """Representation of a blockable client.""" - - _attr_device_class = SwitchDeviceClass.SWITCH - _attr_entity_category = EntityCategory.CONFIG - _attr_has_entity_name = True - _attr_icon = "mdi:ethernet" + entity_description: UnifiEntityDescription _attr_should_poll = False - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up block switch.""" - controller.entities[DOMAIN][BLOCK_SWITCH].add(obj_id) + def __init__( + self, + obj_id: str, + controller: UniFiController, + description: UnifiEntityDescription, + ) -> None: + """Set up UniFi switch entity.""" self._obj_id = obj_id self.controller = controller + self.entity_description = description self._removed = False - client = controller.api.clients[obj_id] - self._attr_available = controller.available - self._attr_is_on = not client.blocked - self._attr_unique_id = f"{BLOCK_SWITCH}-{obj_id}" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, obj_id)}, - default_manufacturer=client.oui, - default_name=client.name or client.hostname, + self._attr_available = description.available_fn(controller, obj_id) + self._attr_device_info = description.device_info_fn(controller.api, obj_id) + self._attr_unique_id = description.unique_id_fn(obj_id) + + obj = description.object_fn(self.controller.api, obj_id) + self._attr_is_on = description.is_on_fn(controller.api, obj) + self._attr_name = description.name_fn(obj) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on switch.""" + await self.entity_description.control_fn( + self.controller.api, self._obj_id, True + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off switch.""" + await self.entity_description.control_fn( + self.controller.api, self._obj_id, False ) async def async_added_to_hass(self) -> None: - """Entity created.""" + """Register callbacks.""" + description = self.entity_description + handler = description.api_handler_fn(self.controller.api) self.async_on_remove( - self.controller.api.clients.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - self.controller.api.events.subscribe( - self.async_event_callback, CLIENT_BLOCKED + CLIENT_UNBLOCKED - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, self.controller.signal_remove, self.remove_item - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, self.controller.signal_options_update, self.options_updated + handler.subscribe( + self.async_signalling_callback, ) ) self.async_on_remove( @@ -355,29 +362,50 @@ class UnifiBlockClientSwitch(SwitchEntity): self.async_signal_reachable_callback, ) ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect object when removed.""" - self.controller.entities[DOMAIN][BLOCK_SWITCH].remove(self._obj_id) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self.controller.signal_options_update, + self.options_updated, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self.controller.signal_remove, + self.remove_item, + ) + ) + if description.event_to_subscribe is not None: + self.async_on_remove( + self.controller.api.events.subscribe( + self.async_event_callback, + description.event_to_subscribe, + ) + ) + if description.custom_subscribe is not None: + self.async_on_remove( + description.custom_subscribe(self.controller.api)( + self.async_signalling_callback, ItemEvent.CHANGED + ), + ) @callback def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Update the clients state.""" - if event == ItemEvent.DELETED: + """Update the switch state.""" + if event == ItemEvent.DELETED and obj_id == self._obj_id: self.hass.async_create_task(self.remove_item({self._obj_id})) return - self._attr_available = self.controller.available - self.async_write_ha_state() - - @callback - def async_event_callback(self, event: Event) -> None: - """Event subscription callback.""" - if event.mac != self._obj_id: + description = self.entity_description + if not description.supported_fn(self.controller.api, self._obj_id): + self.hass.async_create_task(self.remove_item({self._obj_id})) return - if event.key in CLIENT_BLOCKED + CLIENT_UNBLOCKED: - self._attr_is_on = event.key in CLIENT_UNBLOCKED - self._attr_available = self.controller.available + + if not description.only_event_for_state_change: + obj = description.object_fn(self.controller.api, self._obj_id) + self._attr_is_on = description.is_on_fn(self.controller.api, obj) + self._attr_available = description.available_fn(self.controller, self._obj_id) self.async_write_ha_state() @callback @@ -385,30 +413,28 @@ class UnifiBlockClientSwitch(SwitchEntity): """Call when controller connection state change.""" self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on connectivity for client.""" - await self.controller.api.request( - ClientBlockRequest.create(self._obj_id, False) - ) + @callback + def async_event_callback(self, event: Event) -> None: + """Event subscription callback.""" + if event.mac != self._obj_id: + return - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off connectivity for client.""" - await self.controller.api.request(ClientBlockRequest.create(self._obj_id, True)) + description = self.entity_description + assert isinstance(description.event_to_subscribe, tuple) + assert isinstance(description.event_is_on, tuple) - @property - def icon(self) -> str: - """Return the icon to use in the frontend.""" - if not self.is_on: - return "mdi:network-off" - return "mdi:network" + if event.key in description.event_to_subscribe: + self._attr_is_on = event.key in description.event_is_on + self._attr_available = description.available_fn(self.controller, self._obj_id) + self.async_write_ha_state() async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" - if self._obj_id not in self.controller.option_block_clients: + if not self.entity_description.allowed_fn(self.controller, self._obj_id): await self.remove_item({self._obj_id}) async def remove_item(self, keys: set) -> None: - """Remove entity if key is part of set.""" + """Remove entity if object ID is part of set.""" if self._obj_id not in keys or self._removed: return self._removed = True @@ -416,313 +442,3 @@ class UnifiBlockClientSwitch(SwitchEntity): er.async_get(self.hass).async_remove(self.entity_id) else: await self.async_remove(force_remove=True) - - -class UnifiDPIRestrictionSwitch(SwitchEntity): - """Representation of a DPI restriction group.""" - - _attr_entity_category = EntityCategory.CONFIG - - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up dpi switch.""" - controller.entities[DOMAIN][DPI_SWITCH].add(obj_id) - self._obj_id = obj_id - self.controller = controller - - dpi_group = controller.api.dpi_groups[obj_id] - self._known_app_ids = dpi_group.dpiapp_ids - - self._attr_available = controller.available - self._attr_is_on = self.calculate_enabled() - self._attr_name = dpi_group.name - self._attr_unique_id = dpi_group.id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"unifi_controller_{obj_id}")}, - manufacturer=ATTR_MANUFACTURER, - model="UniFi Network", - name="UniFi Network", - ) - - async def async_added_to_hass(self) -> None: - """Register callback to known apps.""" - self.async_on_remove( - self.controller.api.dpi_groups.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - self.controller.api.dpi_apps.subscribe( - self.async_signalling_callback, ItemEvent.CHANGED - ), - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, self.controller.signal_remove, self.remove_item - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, self.controller.signal_options_update, self.options_updated - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect object when removed.""" - self.controller.entities[DOMAIN][DPI_SWITCH].remove(self._obj_id) - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Object has new event.""" - if event == ItemEvent.DELETED: - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - dpi_group = self.controller.api.dpi_groups[self._obj_id] - if not dpi_group.dpiapp_ids: - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - self._attr_available = self.controller.available - self._attr_is_on = self.calculate_enabled() - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - @property - def icon(self): - """Return the icon to use in the frontend.""" - if self.is_on: - return "mdi:network" - return "mdi:network-off" - - def calculate_enabled(self) -> bool: - """Calculate if all apps are enabled.""" - dpi_group = self.controller.api.dpi_groups[self._obj_id] - return all( - self.controller.api.dpi_apps[app_id].enabled - for app_id in dpi_group.dpiapp_ids - if app_id in self.controller.api.dpi_apps - ) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Restrict access of apps related to DPI group.""" - dpi_group = self.controller.api.dpi_groups[self._obj_id] - return await asyncio.gather( - *[ - self.controller.api.request( - DPIRestrictionAppEnableRequest.create(app_id, True) - ) - for app_id in dpi_group.dpiapp_ids - ] - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Remove restriction of apps related to DPI group.""" - dpi_group = self.controller.api.dpi_groups[self._obj_id] - return await asyncio.gather( - *[ - self.controller.api.request( - DPIRestrictionAppEnableRequest.create(app_id, False) - ) - for app_id in dpi_group.dpiapp_ids - ] - ) - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.controller.option_dpi_restrictions: - await self.remove_item({self._attr_unique_id}) - - async def remove_item(self, keys: set) -> None: - """Remove entity if key is part of set.""" - if self._attr_unique_id not in keys: - return - - if self.registry_entry: - er.async_get(self.hass).async_remove(self.entity_id) - else: - await self.async_remove(force_remove=True) - - -class UnifiOutletSwitch(SwitchEntity): - """Representation of a outlet relay.""" - - _attr_device_class = SwitchDeviceClass.OUTLET - _attr_has_entity_name = True - _attr_should_poll = False - - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up UniFi Network entity base.""" - self._device_mac, index = obj_id.split("_", 1) - self._index = int(index) - self._obj_id = obj_id - self.controller = controller - - outlet = self.controller.api.outlets[self._obj_id] - self._attr_name = outlet.name - self._attr_is_on = outlet.relay_state - self._attr_unique_id = f"{self._device_mac}-outlet-{index}" - - device = self.controller.api.devices[self._device_mac] - self._attr_available = controller.available and not device.disabled - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=device.board_revision, - ) - - async def async_added_to_hass(self) -> None: - """Entity created.""" - self.async_on_remove( - self.controller.api.outlets.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Object has new event.""" - device = self.controller.api.devices[self._device_mac] - outlet = self.controller.api.outlets[self._obj_id] - self._attr_available = self.controller.available and not device.disabled - self._attr_is_on = outlet.relay_state - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Enable outlet relay.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetOutletRelayRequest.create(device, self._index, True) - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Disable outlet relay.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetOutletRelayRequest.create(device, self._index, False) - ) - - -class UnifiPoePortSwitch(SwitchEntity): - """Representation of a Power-over-Ethernet source port on an UniFi device.""" - - _attr_device_class = SwitchDeviceClass.OUTLET - _attr_entity_category = EntityCategory.CONFIG - _attr_entity_registry_enabled_default = False - _attr_has_entity_name = True - _attr_icon = "mdi:ethernet" - _attr_should_poll = False - - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up UniFi Network entity base.""" - self._device_mac, index = obj_id.split("_", 1) - self._index = int(index) - self._obj_id = obj_id - self.controller = controller - - port = self.controller.api.ports[self._obj_id] - self._attr_name = f"{port.name} PoE" - self._attr_is_on = port.poe_mode != "off" - self._attr_unique_id = f"{self._device_mac}-poe-{index}" - - device = self.controller.api.devices[self._device_mac] - self._attr_available = controller.available and not device.disabled - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=device.board_revision, - ) - - async def async_added_to_hass(self) -> None: - """Entity created.""" - self.async_on_remove( - self.controller.api.ports.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Object has new event.""" - device = self.controller.api.devices[self._device_mac] - port = self.controller.api.ports[self._obj_id] - self._attr_available = self.controller.available and not device.disabled - self._attr_is_on = port.poe_mode != "off" - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Enable POE for client.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetPoePortModeRequest.create(device, self._index, "auto") - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Disable POE for client.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetPoePortModeRequest.create(device, self._index, "off") - ) - - -UNIFI_LOADERS: tuple[UnifiEntityLoader, ...] = ( - UnifiEntityLoader[Clients]( - allowed_fn=lambda controller, obj_id: obj_id in controller.option_block_clients, - entity_cls=UnifiBlockClientSwitch, - handler_fn=lambda contrlr: contrlr.api.clients, - supported_fn=lambda handler, obj_id: True, - ), - UnifiEntityLoader[DPIRestrictionGroups]( - allowed_fn=lambda controller, obj_id: controller.option_dpi_restrictions, - entity_cls=UnifiDPIRestrictionSwitch, - handler_fn=lambda controller: controller.api.dpi_groups, - supported_fn=lambda handler, obj_id: bool(handler[obj_id].dpiapp_ids), - ), - UnifiEntityLoader[Outlets]( - allowed_fn=lambda controller, obj_id: True, - entity_cls=UnifiOutletSwitch, - handler_fn=lambda controller: controller.api.outlets, - supported_fn=lambda handler, obj_id: handler[obj_id].has_relay, - ), - UnifiEntityLoader[Ports]( - allowed_fn=lambda controller, obj_id: True, - entity_cls=UnifiPoePortSwitch, - handler_fn=lambda controller: controller.api.ports, - supported_fn=lambda handler, obj_id: handler[obj_id].port_poe, - ), -) diff --git a/homeassistant/components/unifi/translations/bg.json b/homeassistant/components/unifi/translations/bg.json index ebe6a494d6c..b9688a023f2 100644 --- a/homeassistant/components/unifi/translations/bg.json +++ b/homeassistant/components/unifi/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0421\u0430\u0439\u0442\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", diff --git a/homeassistant/components/unifi/translations/hr.json b/homeassistant/components/unifi/translations/hr.json index 94a064f34b4..47ba1743f71 100644 --- a/homeassistant/components/unifi/translations/hr.json +++ b/homeassistant/components/unifi/translations/hr.json @@ -1,13 +1,19 @@ { "config": { + "error": { + "service_unavailable": "Povezivanje nije uspjelo" + }, "step": { "user": { "data": { "host": "Host", "password": "Lozinka", "port": "Port", - "username": "Korisni\u010dko ime" - } + "site": "ID lokacije", + "username": "Korisni\u010dko ime", + "verify_ssl": "Provjerite SSL certifikat" + }, + "title": "Postavite UniFi mre\u017eu" } } } diff --git a/homeassistant/components/unifi/translations/sk.json b/homeassistant/components/unifi/translations/sk.json index da71ce60d66..e19794c773a 100644 --- a/homeassistant/components/unifi/translations/sk.json +++ b/homeassistant/components/unifi/translations/sk.json @@ -4,14 +4,19 @@ "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "faulty_credentials": "Neplatn\u00e9 overenie" + "faulty_credentials": "Neplatn\u00e9 overenie", + "service_unavailable": "Nepodarilo sa pripoji\u0165", + "unknown_client_mac": "Na tejto adrese MAC nie je k dispoz\u00edcii \u017eiadny klient" }, + "flow_title": "{site} ({host})", "step": { "user": { "data": { + "host": "Hostite\u013e", "password": "Heslo", "port": "Port", - "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } } diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 30b1d1ad56d..4e659d39cc5 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -5,30 +5,19 @@ import asyncio from datetime import timedelta import logging -from aiohttp import CookieJar from aiohttp.client_exceptions import ServerDisconnectedError -from pyunifiprotect import ProtectApiClient from pyunifiprotect.exceptions import ClientError, NotAuthorized from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers import device_registry as dr, issue_registry as ir +from homeassistant.helpers.issue_registry import IssueSeverity from .const import ( - CONF_ALL_UPDATES, - CONF_OVERRIDE_CHOST, + CONF_ALLOW_EA, DEFAULT_SCAN_INTERVAL, - DEVICES_FOR_SUBSCRIBE, DEVICES_THAT_ADOPT, DOMAIN, MIN_REQUIRED_PROTECT_V, @@ -39,7 +28,11 @@ from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services -from .utils import _async_unifi_mac_from_hass, async_get_devices +from .utils import ( + _async_unifi_mac_from_hass, + async_create_api_client, + async_get_devices, +) from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -51,19 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the UniFi Protect config entries.""" async_start_discovery(hass) - session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) - protect = ProtectApiClient( - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - verify_ssl=entry.data[CONF_VERIFY_SSL], - session=session, - subscribed_models=DEVICES_FOR_SUBSCRIBE, - override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), - ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), - ignore_unadopted=False, - ) + protect = async_create_api_client(hass, entry) _LOGGER.debug("Connect to UniFi Protect") data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) @@ -82,28 +63,75 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - await async_migrate_data(hass, entry, protect) if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac) - await data_service.async_setup() - if not data_service.last_update_success: - raise ConfigEntryNotReady - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async_setup_services(hass) - hass.http.register_view(ThumbnailProxyView(hass)) - hass.http.register_view(VideoProxyView(hass)) - entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) + if ( + not entry.options.get(CONF_ALLOW_EA, False) + and await nvr_info.get_is_prerelease() + ): + ir.async_create_issue( + hass, + DOMAIN, + "ea_warning", + is_fixable=True, + is_persistent=True, + learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + severity=IssueSeverity.WARNING, + translation_key="ea_warning", + translation_placeholders={"version": str(nvr_info.version)}, + data={"entry_id": entry.entry_id}, + ) + + try: + await _async_setup_entry(hass, entry, data_service) + except Exception as err: + if await nvr_info.get_is_prerelease(): + # If they are running a pre-release, its quite common for setup + # to fail so we want to create a repair issue for them so its + # obvious what the problem is. + ir.async_create_issue( + hass, + DOMAIN, + f"ea_setup_failed_{nvr_info.version}", + is_fixable=False, + is_persistent=False, + learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + severity=IssueSeverity.ERROR, + translation_key="ea_setup_failed", + translation_placeholders={ + "error": str(err), + "version": str(nvr_info.version), + }, + ) + ir.async_delete_issue(hass, DOMAIN, "ea_warning") + _LOGGER.exception("Error setting up UniFi Protect integration: %s", err) + raise + return True +async def _async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, data_service: ProtectData +) -> None: + await async_migrate_data(hass, entry, data_service.api) + + await data_service.async_setup() + if not data_service.last_update_success: + raise ConfigEntryNotReady + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + async_setup_services(hass) + hass.http.register_view(ThumbnailProxyView(hass)) + hass.http.register_view(VideoProxyView(hass)) + + async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) @@ -111,6 +139,7 @@ async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> Non async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload UniFi Protect config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): data: ProtectData = hass.data[DOMAIN][entry.entry_id] await data.async_stop() diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 05f7b37d6c8..bf53dc8d206 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -8,13 +8,14 @@ import logging from pyunifiprotect.data import ( NVR, Camera, - Event, Light, ModelType, MountType, ProtectAdoptableDeviceModel, ProtectModelWithId, Sensor, + SmartDetectAudioType, + SmartDetectObjectType, ) from pyunifiprotect.data.nvr import UOSDisk @@ -32,12 +33,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ( - EventThumbnailMixin, + EventEntityMixin, ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities, ) -from .models import PermRequired, ProtectRequiredKeysMixin +from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -51,6 +52,13 @@ class ProtectBinaryEntityDescription( """Describes UniFi Protect Binary Sensor entity.""" +@dataclass +class ProtectBinaryEventEntityDescription( + ProtectEventMixin, BinarySensorEntityDescription +): + """Describes UniFi Protect Binary Sensor entity.""" + + MOUNT_DEVICE_CLASS_MAP = { MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR, MountType.WINDOW: BinarySensorDeviceClass.WINDOW, @@ -59,14 +67,6 @@ MOUNT_DEVICE_CLASS_MAP = { CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( - ProtectBinaryEntityDescription( - key="doorbell", - name="Doorbell", - device_class=BinarySensorDeviceClass.OCCUPANCY, - icon="mdi:doorbell-video", - ufp_required_field="feature_flags.has_chime", - ufp_value="is_ringing", - ), ProtectBinaryEntityDescription( key="dark", name="Is Dark", @@ -179,7 +179,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="smart_face", name="Detections: Face", - icon="mdi:human-greeting", + icon="mdi:mdi-face", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_face", ufp_value="is_face_detection_on", @@ -194,6 +194,24 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_value="is_package_detection_on", ufp_perm=PermRequired.NO_WRITE, ), + ProtectBinaryEntityDescription( + key="smart_licenseplate", + name="Detections: License Plate", + icon="mdi:car", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_license_plate", + ufp_value="is_license_plate_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="smart_smoke", + name="Detections: Smoke/CO", + icon="mdi:fire", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_smoke", + ufp_value="is_smoke_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), ) LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( @@ -313,12 +331,98 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ) -MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( - ProtectBinaryEntityDescription( +EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( + ProtectBinaryEventEntityDescription( + key="doorbell", + name="Doorbell", + device_class=BinarySensorDeviceClass.OCCUPANCY, + icon="mdi:doorbell-video", + ufp_required_field="feature_flags.has_chime", + ufp_value="is_ringing", + ufp_event_obj="last_ring_event", + ), + ProtectBinaryEventEntityDescription( key="motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", + ufp_event_obj="last_motion_event", + ), + ProtectBinaryEventEntityDescription( + key="smart_obj_any", + name="Object Detected", + icon="mdi:eye", + ufp_value="is_smart_detected", + ufp_required_field="feature_flags.has_smart_detect", + ufp_event_obj="last_smart_detect_event", + ), + ProtectBinaryEventEntityDescription( + key="smart_obj_person", + name="Person Detected", + icon="mdi:walk", + ufp_value="is_smart_detected", + ufp_required_field="can_detect_person", + ufp_enabled="is_person_detection_on", + ufp_event_obj="last_smart_detect_event", + ufp_smart_type=SmartDetectObjectType.PERSON, + ), + ProtectBinaryEventEntityDescription( + key="smart_obj_vehicle", + name="Vehicle Detected", + icon="mdi:car", + ufp_value="is_smart_detected", + ufp_required_field="can_detect_vehicle", + ufp_enabled="is_vehicle_detection_on", + ufp_event_obj="last_smart_detect_event", + ufp_smart_type=SmartDetectObjectType.VEHICLE, + ), + ProtectBinaryEventEntityDescription( + key="smart_obj_face", + name="Face Detected", + icon="mdi:mdi-face", + ufp_value="is_smart_detected", + ufp_required_field="can_detect_face", + ufp_enabled="is_face_detection_on", + ufp_event_obj="last_smart_detect_event", + ufp_smart_type=SmartDetectObjectType.FACE, + ), + ProtectBinaryEventEntityDescription( + key="smart_obj_package", + name="Package Detected", + icon="mdi:package-variant-closed", + ufp_value="is_smart_detected", + ufp_required_field="can_detect_package", + ufp_enabled="is_package_detection_on", + ufp_event_obj="last_smart_detect_event", + ufp_smart_type=SmartDetectObjectType.PACKAGE, + ), + ProtectBinaryEventEntityDescription( + key="smart_audio_any", + name="Audio Object Detected", + icon="mdi:eye", + ufp_value="is_smart_detected", + ufp_required_field="feature_flags.has_smart_detect", + ufp_event_obj="last_smart_audio_detect_event", + ), + ProtectBinaryEventEntityDescription( + key="smart_audio_smoke", + name="Smoke Alarm Detected", + icon="mdi:fire", + ufp_value="is_smart_detected", + ufp_required_field="can_detect_smoke", + ufp_enabled="is_smoke_detection_on", + ufp_event_obj="last_smart_audio_detect_event", + ufp_smart_type=SmartDetectAudioType.SMOKE, + ), + ProtectBinaryEventEntityDescription( + key="smart_audio_cmonx", + name="CO Alarm Detected", + icon="mdi:fire", + ufp_value="is_smart_detected", + ufp_required_field="can_detect_smoke", + ufp_enabled="is_smoke_detection_on", + ufp_event_obj="last_smart_audio_detect_event", + ufp_smart_type=SmartDetectAudioType.CMONX, ), ) @@ -382,7 +486,7 @@ async def async_setup_entry( ufp_device=device, ) if device.is_adopted and isinstance(device, Camera): - entities += _async_motion_entities(data, ufp_device=device) + entities += _async_event_entities(data, ufp_device=device) async_add_entities(entities) entry.async_on_unload( @@ -398,14 +502,14 @@ async def async_setup_entry( lock_descs=DOORLOCK_SENSORS, viewer_descs=VIEWER_SENSORS, ) - entities += _async_motion_entities(data) + entities += _async_event_entities(data) entities += _async_nvr_entities(data) async_add_entities(entities) @callback -def _async_motion_entities( +def _async_event_entities( data: ProtectData, ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: @@ -414,7 +518,9 @@ def _async_motion_entities( data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device] ) for device in devices: - for description in MOTION_SENSORS: + for description in EVENT_SENSORS: + if not description.has_required(device): + continue entities.append(ProtectEventBinarySensor(data, device, description)) _LOGGER.debug( "Adding binary sensor entity %s for %s", @@ -508,17 +614,16 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): self._attr_is_on = not self._disk.is_healthy -class ProtectEventBinarySensor(EventThumbnailMixin, ProtectDeviceBinarySensor): - """A UniFi Protect Device Binary Sensor with access tokens.""" +class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): + """A UniFi Protect Device Binary Sensor for events.""" - device: Camera + entity_description: ProtectBinaryEventEntityDescription @callback - def _async_get_event(self) -> Event | None: - """Get event from Protect device.""" - - event: Event | None = None - if self.device.is_motion_detected and self.device.last_motion_event is not None: - event = self.device.last_motion_event - - return event + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + is_on = self.entity_description.get_is_on(device) + self._attr_is_on: bool | None = is_on + if not is_on: + self._event = None + self._attr_extra_state_attributes = {} diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 8f561e5556f..00dbbe77e77 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -175,9 +175,10 @@ class ProtectCamera(ProtectDeviceEntity, Camera): self._stream_source = ( # pylint: disable=attribute-defined-outside-init None if disable_stream else rtsp_url ) - self._attr_supported_features: int = ( - CameraEntityFeature.STREAM if self._stream_source else 0 - ) + if self._stream_source: + self._attr_supported_features = CameraEntityFeature.STREAM + else: + self._attr_supported_features = CameraEntityFeature(0) @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index f07ca923a53..571922d8651 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -34,6 +34,7 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, + CONF_ALLOW_EA, CONF_DISABLE_RTSP, CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, @@ -224,6 +225,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, + CONF_ALLOW_EA: False, }, ) @@ -392,6 +394,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA ), ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), + vol.Optional( + CONF_ALLOW_EA, + default=self.config_entry.options.get(CONF_ALLOW_EA, False), + ): bool, } ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 93a0fa5ff74..df4d8f77d99 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -7,6 +7,7 @@ from homeassistant.const import Platform DOMAIN = "unifiprotect" ATTR_EVENT_SCORE = "event_score" +ATTR_EVENT_ID = "event_id" ATTR_WIDTH = "width" ATTR_HEIGHT = "height" ATTR_FPS = "fps" @@ -20,6 +21,7 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" +CONF_ALLOW_EA = "allow_ea" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/diagnostics.py b/homeassistant/components/unifiprotect/diagnostics.py index b76c9eba1e7..6d4ebcd975d 100644 --- a/homeassistant/components/unifiprotect/diagnostics.py +++ b/homeassistant/components/unifiprotect/diagnostics.py @@ -18,4 +18,5 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] - return cast(dict[str, Any], anonymize_data(data.api.bootstrap.unifi_dict())) + bootstrap = cast(dict[str, Any], anonymize_data(data.api.bootstrap.unifi_dict())) + return {"bootstrap": bootstrap, "options": dict(config_entry.options)} diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 9777ccbd72a..134b55c4b0d 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -24,10 +24,15 @@ from homeassistant.core import callback import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN +from .const import ( + ATTR_EVENT_ID, + ATTR_EVENT_SCORE, + DEFAULT_ATTRIBUTION, + DEFAULT_BRAND, + DOMAIN, +) from .data import ProtectData -from .models import PermRequired, ProtectRequiredKeysMixin -from .utils import get_nested_attr +from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin _LOGGER = logging.getLogger(__name__) @@ -82,10 +87,8 @@ def _async_device_entities( ): continue - if description.ufp_required_field: - required_field = get_nested_attr(device, description.ufp_required_field) - if not required_field: - continue + if not description.has_required(device): + continue entities.append( klass( @@ -294,42 +297,38 @@ class ProtectNVREntity(ProtectDeviceEntity): self._attr_available = self.data.last_update_success -class EventThumbnailMixin(ProtectDeviceEntity): +class EventEntityMixin(ProtectDeviceEntity): """Adds motion event attributes to sensor.""" - def __init__(self, *args: Any, **kwarg: Any) -> None: + entity_description: ProtectEventMixin + + def __init__( + self, + *args: Any, + **kwarg: Any, + ) -> None: """Init an sensor that has event thumbnails.""" super().__init__(*args, **kwarg) self._event: Event | None = None @callback - def _async_get_event(self) -> Event | None: - """Get event from Protect device. - - To be overridden by child classes. - """ - raise NotImplementedError() - - @callback - def _async_thumbnail_extra_attrs(self) -> dict[str, Any]: - # Camera motion sensors with object detection - attrs: dict[str, Any] = { - ATTR_EVENT_SCORE: 0, - } + def _async_event_extra_attrs(self) -> dict[str, Any]: + attrs: dict[str, Any] = {} if self._event is None: return attrs + attrs[ATTR_EVENT_ID] = self._event.id attrs[ATTR_EVENT_SCORE] = self._event.score return attrs @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - self._event = self._async_get_event() + self._event = self.entity_description.get_event_obj(device) attrs = self.extra_state_attributes or {} self._attr_extra_state_attributes = { **attrs, - **self._async_thumbnail_extra_attrs(), + **self._async_event_extra_attrs(), } diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ae37360c5ee..c7259356b66 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -1,10 +1,11 @@ { "domain": "unifiprotect", "name": "UniFi Protect", + "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.3.4", "unifi-discovery==1.1.7"], - "dependencies": ["http"], + "requirements": ["pyunifiprotect==4.5.2", "unifi-discovery==1.1.7"], + "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 6ebb36c11c5..81054d9aff5 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -406,10 +406,13 @@ class ProtectMediaSource(MediaSource): event_text = "Motion Event" elif event_type == EventType.SMART_DETECT.value: if isinstance(event, Event): - smart_type = event.smart_detect_types[0] + smart_types = event.smart_detect_types else: - smart_type = SmartDetectObjectType(event["smartDetectTypes"][0]) - event_text = f"Smart Detection - {smart_type.name.title()}" + smart_types = [ + SmartDetectObjectType(e) for e in event["smartDetectTypes"] + ] + smart_type_names = [s.name.title().replace("_", " ") for s in smart_types] + event_text = f"Smart Detection - {','.join(smart_type_names)}" title += f" {event_text}" nvr = data.api.bootstrap.nvr diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index adab5c032e1..01d4820e9a9 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,9 +5,9 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import Enum import logging -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic, TypeVar, Union, cast -from pyunifiprotect.data import NVR, ProtectAdoptableDeviceModel +from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel from homeassistant.helpers.entity import EntityDescription @@ -54,6 +54,41 @@ class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): return bool(get_nested_attr(obj, self.ufp_enabled)) return True + def has_required(self, obj: T) -> bool: + """Return if has required field.""" + + if self.ufp_required_field is None: + return True + return bool(get_nested_attr(obj, self.ufp_required_field)) + + +@dataclass +class ProtectEventMixin(ProtectRequiredKeysMixin[T]): + """Mixin for events.""" + + ufp_event_obj: str | None = None + ufp_smart_type: str | None = None + + def get_event_obj(self, obj: T) -> Event | None: + """Return value from UniFi Protect device.""" + + if self.ufp_event_obj is not None: + return cast(Event, get_nested_attr(obj, self.ufp_event_obj)) + return None + + def get_is_on(self, obj: T) -> bool: + """Return value if event is active.""" + + value = bool(self.get_ufp_value(obj)) + if value: + event = self.get_event_obj(obj) + value = event is not None + + if event is not None and self.ufp_smart_type is not None: + value = self.ufp_smart_type in event.smart_detect_types + + return value + @dataclass class ProtectSetableKeysMixin(ProtectRequiredKeysMixin[T]): diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py new file mode 100644 index 00000000000..98846113e5e --- /dev/null +++ b/homeassistant/components/unifiprotect/repairs.py @@ -0,0 +1,96 @@ +"""unifiprotect.repairs.""" + +from __future__ import annotations + +from typing import cast + +from pyunifiprotect import ProtectApiClient +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry + +from .const import CONF_ALLOW_EA +from .utils import async_create_api_client + + +class EAConfirm(RepairsFlow): + """Handler for an issue fixing flow.""" + + _api: ProtectApiClient + _entry: ConfigEntry + + def __init__(self, api: ProtectApiClient, entry: ConfigEntry) -> None: + """Create flow.""" + + self._api = api + self._entry = entry + super().__init__() + + @callback + def _async_get_placeholders(self) -> dict[str, str] | None: + issue_registry = async_get_issue_registry(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return description_placeholders + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await self.async_step_start() + + async def async_step_start( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is None: + placeholders = self._async_get_placeholders() + return self.async_show_form( + step_id="start", + data_schema=vol.Schema({}), + description_placeholders=placeholders, + ) + + nvr = await self._api.get_nvr() + if await nvr.get_is_prerelease(): + return await self.async_step_confirm() + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_create_entry(title="", data={}) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + options = dict(self._entry.options) + options[CONF_ALLOW_EA] = True + self.hass.config_entries.async_update_entry(self._entry, options=options) + return self.async_create_entry(title="", data={}) + + placeholders = self._async_get_placeholders() + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=placeholders, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + if data is not None and issue_id == "ea_warning": + entry_id = cast(str, data["entry_id"]) + if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: + api = async_create_api_client(hass, entry) + return EAConfirm(api, entry) + return ConfirmRepairFlow() diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 57bd4fc7230..fa08892e2d3 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -9,13 +9,13 @@ from typing import Any, cast from pyunifiprotect.data import ( NVR, Camera, - Event, Light, ModelType, ProtectAdoptableDeviceModel, ProtectDeviceModel, ProtectModelWithId, Sensor, + SmartDetectObjectType, ) from homeassistant.components.sensor import ( @@ -44,17 +44,18 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ( - EventThumbnailMixin, + EventEntityMixin, ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities, ) -from .models import PermRequired, ProtectRequiredKeysMixin, T +from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin, T from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current _LOGGER = logging.getLogger(__name__) OBJECT_TYPE_NONE = "none" DEVICE_CLASS_DETECTION = "unifiprotect__detection" +DEVICE_CLASS_LICENSE_PLATE = "unifiprotect__license_plate" @dataclass @@ -74,6 +75,13 @@ class ProtectSensorEntityDescription( return value +@dataclass +class ProtectSensorEventEntityDescription( + ProtectEventMixin[T], SensorEntityDescription +): + """Describes UniFi Protect Sensor entity.""" + + def _get_uptime(obj: ProtectDeviceModel) -> datetime | None: if obj.up_since is None: return None @@ -513,11 +521,24 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ) -MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( - ProtectSensorEntityDescription( +EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( + ProtectSensorEventEntityDescription( key="detected_object", name="Detected Object", device_class=DEVICE_CLASS_DETECTION, + entity_registry_enabled_default=False, + ufp_value="is_smart_detected", + ufp_event_obj="last_smart_detect_event", + ), + ProtectSensorEventEntityDescription( + key="smart_obj_licenseplate", + name="License Plate Detected", + icon="mdi:car", + device_class=DEVICE_CLASS_LICENSE_PLATE, + ufp_value="is_smart_detected", + ufp_required_field="can_detect_license_plate", + ufp_event_obj="last_smart_detect_event", + ufp_smart_type=SmartDetectObjectType.LICENSE_PLATE, ), ) @@ -620,7 +641,7 @@ async def async_setup_entry( ufp_device=device, ) if device.is_adopted_by_us and isinstance(device, Camera): - entities += _async_motion_entities(data, ufp_device=device) + entities += _async_event_entities(data, ufp_device=device) async_add_entities(entities) entry.async_on_unload( @@ -638,14 +659,14 @@ async def async_setup_entry( chime_descs=CHIME_SENSORS, viewer_descs=VIEWER_SENSORS, ) - entities += _async_motion_entities(data) + entities += _async_event_entities(data) entities += _async_nvr_entities(data) async_add_entities(entities) @callback -def _async_motion_entities( +def _async_event_entities( data: ProtectData, ufp_device: Camera | None = None, ) -> list[ProtectDeviceEntity]: @@ -666,8 +687,11 @@ def _async_motion_entities( if not device.feature_flags.has_smart_detect: continue - for description in MOTION_SENSORS: - entities.append(ProtectEventSensor(data, device, description)) + for event_desc in EVENT_SENSORS: + if not event_desc.has_required(device): + continue + + entities.append(ProtectEventSensor(data, device, event_desc)) _LOGGER.debug( "Adding sensor entity %s for %s", description.name, @@ -730,30 +754,38 @@ class ProtectNVRSensor(ProtectNVREntity, SensorEntity): self._attr_native_value = self.entity_description.get_ufp_value(self.device) -class ProtectEventSensor(ProtectDeviceSensor, EventThumbnailMixin): +class ProtectEventSensor(EventEntityMixin, SensorEntity): """A UniFi Protect Device Sensor with access tokens.""" - device: Camera + entity_description: ProtectSensorEventEntityDescription - @callback - def _async_get_event(self) -> Event | None: - """Get event from Protect device.""" - - event: Event | None = None - if ( - self.device.is_smart_detected - and self.device.last_smart_detect_event is not None - and len(self.device.last_smart_detect_event.smart_detect_types) > 0 - ): - event = self.device.last_smart_detect_event - - return event + def __init__( + self, + data: ProtectData, + device: ProtectAdoptableDeviceModel, + description: ProtectSensorEventEntityDescription, + ) -> None: + """Initialize an UniFi Protect sensor.""" + super().__init__(data, device, description) @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: # do not call ProtectDeviceSensor method since we want event to get value here - EventThumbnailMixin._async_update_device_from_protect(self, device) - if self._event is None: - self._attr_native_value = OBJECT_TYPE_NONE + EventEntityMixin._async_update_device_from_protect(self, device) + if ( + self.entity_description.ufp_smart_type + == SmartDetectObjectType.LICENSE_PLATE + ): + if ( + self._event is None + or self._event.metadata is None + or self._event.metadata.license_plate is None + ): + self._attr_native_value = OBJECT_TYPE_NONE + else: + self._attr_native_value = self._event.metadata.license_plate.name else: - self._attr_native_value = self._event.smart_detect_types[0].value + if self._event is None: + self._attr_native_value = OBJECT_TYPE_NONE + else: + self._attr_native_value = self._event.smart_detect_types[0].value diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d3cfe24abd2..abac7701279 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -50,9 +50,31 @@ "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", - "max_media": "Max number of event to load for Media Browser (increases RAM usage)" + "max_media": "Max number of event to load for Media Browser (increases RAM usage)", + "allow_ea": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)" } } } + }, + "issues": { + "ea_warning": { + "title": "UniFi Protect v{version} is an Early Access version", + "fix_flow": { + "step": { + "start": { + "title": "v{version} is an Early Access version", + "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect." + }, + "confirm": { + "title": "v{version} is an Early Access version", + "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break." + } + } + } + }, + "ea_setup_failed": { + "title": "Setup error using Early Access version", + "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}" + } } } diff --git a/homeassistant/components/unifiprotect/strings.sensor.json b/homeassistant/components/unifiprotect/strings.sensor.json new file mode 100644 index 00000000000..ccb16e2d445 --- /dev/null +++ b/homeassistant/components/unifiprotect/strings.sensor.json @@ -0,0 +1,7 @@ +{ + "state": { + "unifiprotect__license_plate": { + "none": "Clear" + } + } +} diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 65de9f52913..fa501f6a364 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -126,7 +126,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_bitrate", - name="Overlay: Show Bitrate", + name="Overlay: Show Nerd Mode", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_debug_enabled", @@ -182,6 +182,26 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method="set_package_detection", ufp_perm=PermRequired.WRITE, ), + ProtectSwitchEntityDescription( + key="smart_licenseplate", + name="Detections: License Plate", + icon="mdi:car", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_license_plate", + ufp_value="is_license_plate_detection_on", + ufp_set_method="set_license_plate_detection", + ufp_perm=PermRequired.WRITE, + ), + ProtectSwitchEntityDescription( + key="smart_smoke", + name="Detections: Smoke/CO", + icon="mdi:fire", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_smoke", + ufp_value="is_smoke_detection_on", + ufp_set_method="set_smoke_detection", + ufp_perm=PermRequired.WRITE, + ), ) PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( diff --git a/homeassistant/components/unifiprotect/translations/bg.json b/homeassistant/components/unifiprotect/translations/bg.json index c5b9cada9e0..924204073df 100644 --- a/homeassistant/components/unifiprotect/translations/bg.json +++ b/homeassistant/components/unifiprotect/translations/bg.json @@ -34,16 +34,19 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0441\u043f\u0438\u0441\u044a\u043a \u0441 MAC \u0430\u0434\u0440\u0435\u0441\u0438, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438 \u0441\u044a\u0441 \u0437\u0430\u043f\u0435\u0442\u0430\u0438" - }, - "step": { - "init": { - "data": { - "ignored_devices": "\u0421\u043f\u0438\u0441\u044a\u043a \u0441 MAC \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0430\u0442, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d \u0441\u044a\u0441 \u0437\u0430\u043f\u0435\u0442\u0430\u0438" + "issues": { + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "title": "v{version} \u0435 \u0432\u0435\u0440\u0441\u0438\u044f \u0441 \u0440\u0430\u043d\u0435\u043d \u0434\u043e\u0441\u0442\u044a\u043f" + }, + "start": { + "title": "v{version} \u0435 \u0432\u0435\u0440\u0441\u0438\u044f \u0441 \u0440\u0430\u043d\u0435\u043d \u0434\u043e\u0441\u0442\u044a\u043f" + } } - } + }, + "title": "UniFi Protect v{version} \u0435 \u0432\u0435\u0440\u0441\u0438\u044f \u0441 \u0440\u0430\u043d\u0435\u043d \u0434\u043e\u0441\u0442\u044a\u043f" } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 1d43956510a..31a15c62848 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -41,16 +41,30 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Ha de ser una llista d'adreces MAC separades per comes" + "issues": { + "ea_setup_failed": { + "title": "Error de configuraci\u00f3 amb la versi\u00f3 d'acc\u00e9s anticipat" }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "title": "v{version} \u00e9s una versi\u00f3 d'acc\u00e9s anticipat" + }, + "start": { + "title": "v{version} \u00e9s una versi\u00f3 d'acc\u00e9s anticipat" + } + } + }, + "title": "UniFi Protect v{version} \u00e9s una versi\u00f3 d'acc\u00e9s anticipat" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "M\u00e8triques en temps real (ALERTA: augmenta considerablement l'\u00fas de CPU)", "disable_rtsp": "Desactiva el flux RTSP", - "ignored_devices": "Llista d'adreces MAC dels dispositius a ignorar, separades per comes", "max_media": "Nombre m\u00e0xim d'esdeveniments a carregar al navegador multim\u00e8dia (augmenta l'\u00fas de RAM)", "override_connection_host": "Substitueix l'amfitri\u00f3 de connexi\u00f3" }, diff --git a/homeassistant/components/unifiprotect/translations/cs.json b/homeassistant/components/unifiprotect/translations/cs.json index 855cc6da6da..fa05d98a41c 100644 --- a/homeassistant/components/unifiprotect/translations/cs.json +++ b/homeassistant/components/unifiprotect/translations/cs.json @@ -32,5 +32,33 @@ } } } + }, + "issues": { + "ea_setup_failed": { + "title": "Chyba instalace pomoc\u00ed verze p\u0159edb\u011b\u017en\u00e9ho p\u0159\u00edstupu" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Opravdu chcete spou\u0161t\u011bt nepodporovan\u00e9 verze UniFi Protect? To m\u016f\u017ee zp\u016fsobit p\u0159eru\u0161en\u00ed integrace Home Assistanta.", + "title": "v{version} je verze p\u0159edb\u011b\u017en\u00e9ho p\u0159\u00edstupu" + }, + "start": { + "description": "Pou\u017e\u00edv\u00e1te v{version} UniFi Protect, co\u017e je verze p\u0159edb\u011b\u017en\u00e9ho p\u0159\u00edstupu. [Verze s p\u0159edb\u011b\u017en\u00fdm p\u0159\u00edstupem nejsou podporov\u00e1ny Home Assistantem](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) a doporu\u010dujeme se co nejd\u0159\u00edve vr\u00e1tit ke stabiln\u00edmu vyd\u00e1n\u00ed. \n\n Odesl\u00e1n\u00edm tohoto formul\u00e1\u0159e bu\u010f [p\u0159ejdete na ni\u017e\u0161\u00ed verzi UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect), nebo souhlas\u00edte se spu\u0161t\u011bn\u00edm nepodporovan\u00e9 verze UniFi Protect.", + "title": "v{version} je verze p\u0159edb\u011b\u017en\u00e9ho p\u0159\u00edstupu" + } + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_ea": "Povolit verze s p\u0159edb\u011b\u017en\u00fdm p\u0159\u00edstupem Protect (VAROV\u00c1N\u00cd: Va\u0161i integraci ozna\u010d\u00ed jako nepodporovanou)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json index f44ad32a2b3..8cb94d2cc01 100644 --- a/homeassistant/components/unifiprotect/translations/de.json +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -21,12 +21,12 @@ }, "reauth_confirm": { "data": { - "host": "IP/Host des UniFi Protect-Servers", + "host": "IP/Host des UniFi Protect Servers", "password": "Passwort", "port": "Port", "username": "Benutzername" }, - "title": "UniFi Protect Reauth" + "title": "UniFi Protect Reauthentifizierung" }, "user": { "data": { @@ -37,25 +37,47 @@ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "Du ben\u00f6tigst einen lokalen Benutzer, den du in deiner UniFi OS-Konsole angelegt hast, um sich damit anzumelden. Ubiquiti Cloud-Benutzer funktionieren nicht. F\u00fcr weitere Informationen: {local_user_documentation_url}", - "title": "UniFi Protect-Einrichtung" + "title": "UniFi Protect Einrichtung" } } }, - "options": { - "error": { - "invalid_mac_list": "Muss eine durch Kommas getrennte Liste von MAC-Adressen sein" + "issues": { + "deprecate_smart_sensor": { + "description": "Der einheitliche Sensor \u201eErkanntes Objekt\u201c f\u00fcr intelligente Erkennungen ist jetzt veraltet. Er wurde durch einzelne bin\u00e4re Smart Detection Sensoren f\u00fcr jeden Smart Detection Typ ersetzt. Bitte aktualisiere alle Vorlagen oder Automatisierungen entsprechend.", + "title": "Smart Detection Sensor veraltet" }, + "ea_setup_failed": { + "description": "Du verwendest v{version} von UniFi Protect, eine Early-Access-Version. Beim Versuch, die Integration zu laden, ist ein nicht behebbarer Fehler aufgetreten. Bitte f\u00fchre ein [Downgrade auf eine stabile Version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) von UniFi Protect durch, um die Integration weiterhin zu verwenden. \n\nFehler: {error}", + "title": "Einrichtungsfehler bei Verwendung der Early-Access Version" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "M\u00f6chtest du wirklich nicht unterst\u00fctzte Versionen von UniFi Protect ausf\u00fchren? Dies kann dazu f\u00fchren, dass deine Home Assistant-Integration nicht mehr funktioniert.", + "title": "v{version} ist eine Early-Access Version" + }, + "start": { + "description": "Du verwendest v{version} von UniFi Protect, eine Early-Access-Version. [Early-Access-Versionen werden von Home Assistant nicht unterst\u00fctzt](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) und es wird empfohlen, so bald wie m\u00f6glich zu einer stabilen Version zur\u00fcckzukehren. \n\nDurch das Absenden dieses Formulars hast du entweder [UniFi Protect heruntergestuft](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) oder du stimmst zu, eine nicht unterst\u00fctzte Version von UniFi Protect auszuf\u00fchren.", + "title": "v{version} ist eine Early-Access Version" + } + } + }, + "title": "UniFi Protect v{version} ist eine Early Access Version" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Echtzeitmetriken (WARNUNG: Erh\u00f6ht die CPU-Auslastung erheblich)", + "allow_ea": "Early-Access-Versionen von Protect zulassen (WARNUNG: Markiert deine Integration als nicht unterst\u00fctzt)", "disable_rtsp": "RTSP-Stream deaktivieren", - "ignored_devices": "Kommagetrennte Liste von MAC-Adressen von Ger\u00e4ten, die ignoriert werden sollen", "max_media": "Maximale Anzahl von Ereignissen, die f\u00fcr den Medienbrowser geladen werden (erh\u00f6ht die RAM-Nutzung)", "override_connection_host": "Verbindungshost \u00fcberschreiben" }, "description": "Die Option Echtzeit-Metriken sollte nur aktiviert werden, wenn du die Diagnosesensoren aktiviert hast und diese in Echtzeit aktualisiert werden sollen. Wenn sie nicht aktiviert ist, werden sie nur einmal alle 15 Minuten aktualisiert.", - "title": "UniFi Protect-Optionen" + "title": "UniFi Protect Optionen" } } } diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index 58da67d9383..9c2d33e15d6 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -41,16 +41,34 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03c9\u03bd MAC \u03c0\u03bf\u03c5 \u03c7\u03c9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1." + "issues": { + "ea_setup_failed": { + "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 v{version} \u03c4\u03bf\u03c5 UniFi Protect \u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Early Access. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b9\u03bc\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2. \u039a\u03ac\u03bd\u03c4\u03b5 [\u03c5\u03c0\u03bf\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) \u03c4\u03bf\u03c5 UniFi Protect \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a3\u03c6\u03ac\u03bb\u03bc\u03b1: {error}", + "title": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 Early Access" }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03b2\u03ad\u03b2\u03b1\u03b9\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b5\u03c2 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2 \u03c4\u03bf\u03c5 UniFi Protect; \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03c1\u03bf\u03ba\u03b1\u03bb\u03ad\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant.", + "title": "\u03a4\u03bf v{version} \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Early Access" + }, + "start": { + "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd v{version} \u03c4\u03bf\u03c5 UniFi Protect \u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Early Access. [\u039f\u03b9 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2 Early Access \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) \u03ba\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf \u03c3\u03c5\u03bd\u03c4\u03bf\u03bc\u03cc\u03c4\u03b5\u03c1\u03bf \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03bd. \n\n \u039c\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03af\u03c4\u03b5 [\u03c5\u03c0\u03bf\u03b2\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) \u03b5\u03af\u03c4\u03b5 \u03c3\u03c5\u03bc\u03c6\u03c9\u03bd\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 UniFi Protect.", + "title": "\u03a4\u03bf v{version} \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Early Access" + } + } + }, + "title": "\u03a4\u03bf UniFi Protect v{version} \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c0\u03c1\u03ce\u03b9\u03bc\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "\u039c\u03b5\u03c4\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf (\u03a0\u03a1\u039f\u0395\u0399\u0394\u039f\u03a0\u039f\u0399\u0397\u03a3\u0397: \u0391\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ac \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 CPU)", + "allow_ea": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2 Early Access \u03c4\u03bf\u03c5 Protect (\u03a0\u03a1\u039f\u0395\u0399\u0394\u039f\u03a0\u039f\u0399\u0397\u03a3\u0397: \u0398\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03b1\u03bd\u03b8\u03b5\u03af \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c9\u03c2 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7)", "disable_rtsp": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03bf\u03ae RTSP", - "ignored_devices": "\u039a\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03c9\u03bd MAC \u03c4\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd \u03bc\u03b5 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03cc \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2", "max_media": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c2 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd (\u03b1\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 RAM)", "override_connection_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index c6050d05284..65a398375fe 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -41,16 +41,34 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" + "issues": { + "ea_setup_failed": { + "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}", + "title": "Setup error using Early Access version" }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break.", + "title": "v{version} is an Early Access version" + }, + "start": { + "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect.", + "title": "v{version} is an Early Access version" + } + } + }, + "title": "UniFi Protect v{version} is an Early Access version" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", + "allow_ea": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)", "disable_rtsp": "Disable the RTSP stream", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index e278fb6ecf0..f73a5db7c8b 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -41,16 +41,38 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Debe ser una lista de direcciones MAC separadas por comas" + "issues": { + "deprecate_smart_sensor": { + "description": "El sensor unificado \"Objeto detectado\" para detecciones inteligentes ahora est\u00e1 obsoleto. Ha sido reemplazado por sensores binarios de detecci\u00f3n inteligente individuales para cada tipo de detecci\u00f3n inteligente. Actualiza cualquier plantilla o automatizaci\u00f3n en consecuencia.", + "title": "Sensor de detecci\u00f3n inteligente obsoleto" }, + "ea_setup_failed": { + "description": "Est\u00e1s utilizando v{version} de UniFi Protect, que es una versi\u00f3n de Early Access. Se produjo un error irrecuperable al intentar cargar la integraci\u00f3n. Por favor, [cambia a una versi\u00f3n estable](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) de UniFi Protect para continuar usando la integraci\u00f3n. \n\nError: {error}", + "title": "Error de configuraci\u00f3n al usar la versi\u00f3n Early Access" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u00bfEst\u00e1s seguro de que deseas ejecutar versiones no compatibles de UniFi Protect? Esto puede causar que la integraci\u00f3n de Home Assistant se rompa.", + "title": "v{version} es una versi\u00f3n de Early Access" + }, + "start": { + "description": "Est\u00e1s utilizando v{version} de UniFi Protect, que es una versi\u00f3n Early Access. [Las versiones Early Access no son compatibles con Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) y se recomienda volver a una versi\u00f3n estable tan pronto como sea posible. \n\nAl enviar este formulario, tienes [UniFi Protect degradado](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) o aceptas ejecutar una versi\u00f3n no compatible de UniFi Protect.", + "title": "v{version} es una versi\u00f3n Early Access" + } + } + }, + "title": "UniFi Protect v{version} es una versi\u00f3n Early Access" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: aumenta considerablemente el uso de la CPU)", + "allow_ea": "Permitir versiones Early Access de Protect (ADVERTENCIA: marcar\u00e1 tu integraci\u00f3n como no admitida)", "disable_rtsp": "Deshabilitar la transmisi\u00f3n RTSP", - "ignored_devices": "Lista separada por comas de direcciones MAC de dispositivos para ignorar", "max_media": "N\u00famero m\u00e1ximo de eventos a cargar para el Navegador de Medios (aumenta el uso de RAM)", "override_connection_host": "Anular la conexi\u00f3n del host" }, diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index 4bd402fa1c2..f1a3ca6cf12 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -41,16 +41,38 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Peab olema komadega eraldatud MAC-aadresside loend" + "issues": { + "deprecate_smart_sensor": { + "description": "\u00dchtse \"Avastatud objekti\" andur arukate tuvastuste jaoks on n\u00fc\u00fcdseks kaotanud kehtivuse. See on asendatud iga aruka avastamise t\u00fc\u00fcbi jaoks eraldi aruka avastamise binaarsete anduritega. Palun ajakohasta vastavalt k\u00f5ik mallid v\u00f5i automaatika.", + "title": "Nutika tuvastamise andur on aegunud" }, + "ea_setup_failed": { + "description": "Kasutad UniFi Protecti v {version} mis on varajase juurdep\u00e4\u00e4su versioon. Sidumise laadimisel ilmnes parandamatu viga. Sidumise kasutamise j\u00e4tkamiseks [alanda UniFi Protecti stabiilsele versioonile](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect). \n\n Viga: {error}", + "title": "Varajase juurdep\u00e4\u00e4su versiooni h\u00e4\u00e4lestamise t\u00f5rge" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Kas oled kindel, et soovid k\u00e4itada UniFi Protecti toetamata versioone? See v\u00f5ib p\u00f5hjustada Home Assistanti sidumise katkemise.", + "title": "v {version} on varajase juurdep\u00e4\u00e4su versioon" + }, + "start": { + "description": "Kasutad UniFi Protecti v {version} mis on varajase juurdep\u00e4\u00e4su versioon. [Home Assistant ei toeta varajase juurdep\u00e4\u00e4su versioone](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) ja soovitatav on naasta stabiilsele versioonile niipea kui v\u00f5imalik. \n\n Selle vormi esitades oled kas [UniFi Protecti madalamale versioonile \u00fcle l\u00e4inud](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) v\u00f5i n\u00f5ustud kasutama UniFi Protecti toetamata versiooni.", + "title": "v {version} on varajase juurdep\u00e4\u00e4su versioon" + } + } + }, + "title": "v{version} on UniFi Protecti varajase juurdep\u00e4\u00e4su versioon" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Reaalajas m\u00f5\u00f5dikud (HOIATUS: suurendab oluliselt CPU kasutust)", + "allow_ea": "Protecti varajase juurdep\u00e4\u00e4su versioonide lubamine (HOIATUS: m\u00e4rgib sidumise mitte toetatuks)", "disable_rtsp": "Keela RTSP voog", - "ignored_devices": "Komaga eraldatud loend nende seadmete MAC-aadressidest mida eirata", "max_media": "Meediumibrauserisse laaditavate s\u00fcndmuste maksimaalne arv (suurendab RAM-i kasutamist)", "override_connection_host": "\u00dchenduse hosti alistamine" }, diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index 8cb4b819715..c6527d39a39 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -42,15 +42,11 @@ } }, "options": { - "error": { - "invalid_mac_list": "Doit \u00eatre une liste d'adresses MAC s\u00e9par\u00e9es par des virgules" - }, "step": { "init": { "data": { "all_updates": "M\u00e9triques en temps r\u00e9el (AVERTISSEMENT\u00a0: augmente consid\u00e9rablement l'utilisation du processeur)", "disable_rtsp": "D\u00e9sactiver le flux RTSP", - "ignored_devices": "Liste s\u00e9par\u00e9e par des virgules des adresses MAC des appareils \u00e0 ignorer", "max_media": "Nombre maximal d'\u00e9v\u00e9nements \u00e0 charger pour le navigateur multim\u00e9dia (augmente l'utilisation de la RAM)", "override_connection_host": "Ignorer l'h\u00f4te de connexion" }, diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index dac543f97c2..600eeec24c7 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -41,16 +41,34 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Egy list\u00e1ra van sz\u00fcks\u00e9g, melyben a MAC-c\u00edmek vessz\u0151vel vannak elv\u00e1lasztva" + "issues": { + "ea_setup_failed": { + "description": "Az UniFi Protect {version}. verzi\u00f3j\u00e1t haszn\u00e1lja, amely egy korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3. Helyrehozhatatlan hiba t\u00f6rt\u00e9nt az integr\u00e1ci\u00f3 bet\u00f6lt\u00e9se k\u00f6zben. K\u00e9rem, [haszn\u00e1ljon stabil verzi\u00f3t](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) az integr\u00e1ci\u00f3 tov\u00e1bbi haszn\u00e1lat\u00e1hoz.\n\nHiba: {error}", + "title": "Be\u00e1ll\u00edt\u00e1si hiba a korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3 haszn\u00e1lat\u00e1val" }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Biztos, hogy az UniFi Protect nem t\u00e1mogatott verzi\u00f3it szeretn\u00e9 futtatni? Ez a Home Assistant integr\u00e1ci\u00f3j\u00e1nak meghib\u00e1sod\u00e1s\u00e1t okozhatja.", + "title": "{version}. verzi\u00f3 egy korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3" + }, + "start": { + "description": "Az UniFi Protect {version}. verzi\u00f3j\u00e1t haszn\u00e1lja, amely egy korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3. [A korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3kat a Home Assistant nem t\u00e1mogatja] (https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) \u00e9s aj\u00e1nlott a lehet\u0151 leghamarabb visszat\u00e9rni a stabil verzi\u00f3j\u00e1hoz.", + "title": "{version}. verzi\u00f3 egy korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3" + } + } + }, + "title": "{version} UniFi Protect korai hozz\u00e1f\u00e9r\u00e9si verzi\u00f3" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mok (FIGYELEM: nagym\u00e9rt\u00e9kben n\u00f6veli a CPU terhel\u00e9st)", + "allow_ea": "A Protect korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3inak enged\u00e9lyez\u00e9se (FIGYELMEZTET\u00c9S: Az integr\u00e1ci\u00f3t nem t\u00e1mogatottk\u00e9nt jel\u00f6li meg)", "disable_rtsp": "Az RTSP adatfolyam letilt\u00e1sa", - "ignored_devices": "A figyelmen k\u00edv\u00fcl hagyand\u00f3 eszk\u00f6z\u00f6k MAC-c\u00edm\u00e9nek vessz\u0151vel elv\u00e1lasztott list\u00e1ja", "max_media": "A m\u00e9diab\u00f6ng\u00e9sz\u0151be bet\u00f6ltend\u0151 esem\u00e9nyek maxim\u00e1lis sz\u00e1ma (n\u00f6veli a RAM-haszn\u00e1latot)", "override_connection_host": "Kapcsolat c\u00edm\u00e9nek fel\u00fclb\u00edr\u00e1l\u00e1sa" }, diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index 772c339f3df..6fcaf1c19fb 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -41,16 +41,38 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Harus berupa daftar alamat MAC yang dipisahkan dengan koma" + "issues": { + "deprecate_smart_sensor": { + "description": "Sensor \"Objek Terdeteksi\" terpadu untuk deteksi cerdas sekarang sudah tidak digunakan lagi. Sensor ini telah diganti dengan sensor biner deteksi cerdas individual untuk setiap jenis deteksi cerdas. Perbarui semua templat atau otomasi yang terkait.", + "title": "Sensor Deteksi Cerdas Tidak Digunakan Lagi" }, + "ea_setup_failed": { + "description": "Anda menggunakan v{version} dari UniFi Protect yang merupakan versi Early Access. Terjadi kesalahan yang tidak dapat dipulihkan saat mencoba memuat integrasi. Silakan [turunkan ke versi stabil](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) dari UniFi Protect untuk terus menggunakan integrasi.\n\nKesalahan: {error}", + "title": "Kesalahan penyiapan menggunakan versi Early Access" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Yakin ingin menjalankan versi UniFi Protect yang tidak didukung? Ini dapat menyebabkan integrasi Home Assistant Anda rusak.", + "title": "v{version} adalah versi Early Access" + }, + "start": { + "description": "Anda menggunakan v{version} UniFi Protect yang merupakan versi Early Access. [Versi Early Access tidak didukung oleh Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) dan disarankan untuk kembali ke rilis stabil sesegera mungkin.\n\nDengan mengirimkan formulir ini, Anda seharusnya telah [menurunkan versi UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) atau Anda setuju untuk menjalankan versi UniFi Protect yang tidak didukung.", + "title": "v{version} adalah versi Early Access" + } + } + }, + "title": "UniFi Protect v{version} adalah versi Early Access" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Metrik waktu nyata (PERINGATAN: Meningkatkan penggunaan CPU)", + "allow_ea": "Izinkan versi Early Access UniFi Protect (PERINGATAN: Akan menandai integrasi Anda sebagai tidak didukung)", "disable_rtsp": "Nonaktifkan aliran RTSP", - "ignored_devices": "Daftar alamat MAC perangkat yang dipisahkan koma untuk diabaikan", "max_media": "Jumlah maksimum peristiwa yang akan dimuat untuk Browser Media (meningkatkan penggunaan RAM)", "override_connection_host": "Timpa Host Koneksi" }, diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 712df7b7f2a..d37fbcea83c 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -41,16 +41,34 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Deve essere un elenco di indirizzi MAC separati da virgole" + "issues": { + "ea_setup_failed": { + "description": "Stai utilizzando v{version} di UniFi Protect che \u00e8 una versione ad accesso anticipato. Si \u00e8 verificato un errore irreversibile durante il tentativo di caricare l'integrazione. [Esegui la retrocessione ad una versione stabile](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) di UniFi Protect per continuare ad utilizzare l'integrazione. \n\nErrore: {error}", + "title": "Errore di configurazione durante l'utilizzo della versione ad accesso anticipato" }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Sei sicuro di voler eseguire versioni non supportate di UniFi Protect? Ci\u00f2 potrebbe causare l'interruzione dell'integrazione di Home Assistant.", + "title": "v{version} \u00e8 una versione in accesso anticipato" + }, + "start": { + "description": "Stai utilizzando la v{version} di UniFi Protect che \u00e8 una versione ad accesso anticipato. [Le versioni ad accesso anticipato non sono supportate da Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) e si consiglia di tornare a una versione stabile non appena possibile. \n\nInviando questo modulo hai [retrocesso UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) oppure accetti di eseguire una versione non supportata di UniFi Protect.", + "title": "v{version} \u00e8 una versione in accesso anticipato" + } + } + }, + "title": "UniFi Protect v{version} \u00e8 una versione in accesso anticipato" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Metriche in tempo reale (ATTENZIONE: aumenta notevolmente l'utilizzo della CPU)", + "allow_ea": "Consenti versioni ad accesso anticipato di Protect (ATTENZIONE: l'integrazione verr\u00e0 contrassegnata come non supportata)", "disable_rtsp": "Disabilita il flusso RTSP", - "ignored_devices": "Elenco separato da virgole di indirizzi MAC di dispositivi da ignorare", "max_media": "Numero massimo di eventi da caricare per Media Browser (aumenta l'utilizzo della RAM)", "override_connection_host": "Sostituisci host di connessione" }, diff --git a/homeassistant/components/unifiprotect/translations/ja.json b/homeassistant/components/unifiprotect/translations/ja.json index b55c9c53e84..711e039edaf 100644 --- a/homeassistant/components/unifiprotect/translations/ja.json +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -42,15 +42,11 @@ } }, "options": { - "error": { - "invalid_mac_list": "\u30ab\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305fMAC\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30b9\u30c8\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" - }, "step": { "init": { "data": { "all_updates": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af(Realtime metrics)(\u8b66\u544a: CPU\u4f7f\u7528\u7387\u304c\u5927\u5e45\u306b\u5897\u52a0\u3057\u307e\u3059)", "disable_rtsp": "RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u7121\u52b9\u306b\u3059\u308b", - "ignored_devices": "\u7121\u8996\u3059\u308b\u6a5f\u5668\u306eMAC\u30a2\u30c9\u30ec\u30b9\u306e\u30ab\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8", "max_media": "\u30e1\u30c7\u30a3\u30a2\u30d6\u30e9\u30a6\u30b6\u306b\u30ed\u30fc\u30c9\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u306e\u6700\u5927\u6570(RAM\u4f7f\u7528\u91cf\u304c\u5897\u52a0)", "override_connection_host": "\u63a5\u7d9a\u30db\u30b9\u30c8\u3092\u4e0a\u66f8\u304d" }, diff --git a/homeassistant/components/unifiprotect/translations/nl.json b/homeassistant/components/unifiprotect/translations/nl.json index a5b834fc4d4..d106b9df2f8 100644 --- a/homeassistant/components/unifiprotect/translations/nl.json +++ b/homeassistant/components/unifiprotect/translations/nl.json @@ -41,11 +41,32 @@ } } }, + "issues": { + "ea_setup_failed": { + "description": "Je gebruikt v{version} van UniFi Protect wat een Early Access versie is. Een onherstelbare fout is opgetreden terwijl de integratie aan het laden was. Ga [terug naar een stabiele versie](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) van UniFi Protect om de integratie binnen Home Assistant te kunnen gebruiken.\n\nFout: {error}", + "title": "Installatiefout bij het gebruik van een Early Access versie" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Weet je zeker dat je een niet ondersteunde versie van Unify Protect wilt gebruiken? Dit kan veroorzaken dat je Home Assistant integratie vastloopt.", + "title": "v{version} is een Early Access versie" + }, + "start": { + "description": "Je gebruikt v{version} van UniFi Protect wat een Early Access versie is. [Early Access versies zijn niet ondersteund door Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) en het is aanbevolen om zo gauw mogelijk terug te gaan naar een stabiele versie.\n\nMet het verzenden van dit formulier beveistig je dat je [UniFi Protect hebt gedowngraded](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of je stemt in met het gebruik van een niet ondersteunde versie van UniFi Protect.", + "title": "v{version} is een Early Access versie" + } + } + } + } + }, "options": { "step": { "init": { "data": { "all_updates": "Realtime metrieken (WAARSCHUWING: Verhoogt CPU gebruik)", + "allow_ea": "Toestaan van `Allow Early Access` versies van Protect (WAARSCHUWING: Dit zal je integratie als niet ondersteund markeren)", "disable_rtsp": "De RTSP-stream uitschakelen", "override_connection_host": "Verbindingshost overschrijven" }, diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index 7eac0b2dca1..195b7da31a4 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -41,16 +41,38 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "M\u00e5 v\u00e6re en liste over MAC-adresser atskilt med komma" + "issues": { + "deprecate_smart_sensor": { + "description": "Den enhetlige \u00abDetected Object\u00bb-sensoren for smarte deteksjoner er n\u00e5 avviklet. Den er erstattet med individuelle smartdeteksjonsbin\u00e6re sensorer for hver smartdeteksjonstype. Oppdater eventuelle maler eller automatiseringer tilsvarende.", + "title": "Smart deteksjonssensor avviklet" }, + "ea_setup_failed": { + "description": "Du bruker v {version} av UniFi Protect som er en tidlig tilgangsversjon. Det oppstod en uopprettelig feil under fors\u00f8k p\u00e5 \u00e5 laste integrasjonen. Vennligst [nedgrader til en stabil versjon](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) av UniFi Protect for \u00e5 fortsette \u00e5 bruke integrasjonen. \n\n Feil: {error}", + "title": "Konfigurasjonsfeil ved bruk av tidlig tilgangsversjon" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Er du sikker p\u00e5 at du vil kj\u00f8re versjoner av UniFi Protect som ikke st\u00f8ttes? Dette kan f\u00f8re til at Home Assistant-integrasjonen din g\u00e5r i stykker.", + "title": "v {version} er en tidlig tilgangsversjon" + }, + "start": { + "description": "Du bruker v {version} av UniFi Protect som er en tidlig tilgangsversjon. [Early Access-versjoner st\u00f8ttes ikke av Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), og det anbefales \u00e5 g\u00e5 tilbake til en stabil utgivelse s\u00e5 snart som mulig. \n\n Ved \u00e5 sende inn dette skjemaet har du enten [nedgradert UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) eller du godtar \u00e5 kj\u00f8re en ust\u00f8ttet versjon av UniFi Protect.", + "title": "v {version} er en tidlig tilgangsversjon" + } + } + }, + "title": "UniFi Protect v {version} er en versjon med tidlig tilgang" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Sanntidsm\u00e5linger (ADVARSEL: \u00d8ker CPU-bruken betraktelig)", + "allow_ea": "Tillat tidlig tilgangsversjoner av Protect (ADVARSEL: Vil merke integrasjonen din som ikke st\u00f8ttet)", "disable_rtsp": "Deaktiver RTSP-str\u00f8mmen", - "ignored_devices": "Kommadelt liste over MAC-adresser til enheter som skal ignoreres", "max_media": "Maks antall hendelser som skal lastes for medienettleseren (\u00f8ker RAM-bruken)", "override_connection_host": "Overstyr tilkoblingsvert" }, diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index a0c4e7eb72c..03175f0728b 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -41,16 +41,34 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Musi to by\u0107 lista adres\u00f3w MAC oddzielonych przecinkami" + "issues": { + "ea_setup_failed": { + "description": "U\u017cywasz wersji {version} UniFi Protect, kt\u00f3ra jest wersj\u0105 Early Access. Wyst\u0105pi\u0142 nienaprawialny b\u0142\u0105d podczas pr\u00f3by za\u0142adowania integracji. Aby kontynuowa\u0107 korzystanie z integracji, [zmie\u0144 wersj\u0119 na stabiln\u0105](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) UniFi Protect. \n\nB\u0142\u0105d: {error}", + "title": "B\u0142\u0105d konfiguracji w wersji Early Access" }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Czy na pewno chcesz uruchomi\u0107 nieobs\u0142ugiwane wersje UniFi Protect? Mo\u017ce to popsu\u0107 integracj\u0119 Home Assistant.", + "title": "Wersja {version} jest wersj\u0105 Early Access" + }, + "start": { + "description": "U\u017cywasz wersji {version} UniFi Protect, kt\u00f3ra jest wersj\u0105 Early Access. [Wersje Early Access nie s\u0105 obs\u0142ugiwane przez Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) i zaleca si\u0119 jak najszybszy powr\u00f3t do stabilnej wersji.\n\nPrzesy\u0142aj\u0105c ten formularz, [obni\u017cy\u0142e\u015b wersj\u0119 UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) albo zgadzasz si\u0119 na uruchomienie nieobs\u0142ugiwanej wersji UniFi Protect.", + "title": "Wersja {version} jest wersj\u0105 Early Access" + } + } + }, + "title": "UniFi Protect v{version} to wersja Early Access" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "Metryki w czasie rzeczywistym (UWAGA: Znacznie zwi\u0119ksza u\u017cycie CPU)", + "allow_ea": "Zezw\u00f3l na wersje UniFi Protect Early Access (OSTRZE\u017bENIE: Twoja integracja zostanie oznaczona jako nieobs\u0142ugiwana)", "disable_rtsp": "Wy\u0142\u0105cz strumie\u0144 RTSP", - "ignored_devices": "Oddzielona przecinkami lista adres\u00f3w MAC urz\u0105dze\u0144 do zignorowania", "max_media": "Maksymalna liczba zdarze\u0144 do za\u0142adowania dla przegl\u0105darki medi\u00f3w (zwi\u0119ksza u\u017cycie pami\u0119ci RAM)", "override_connection_host": "Zast\u0105p host po\u0142\u0105czenia" }, diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 3bc780b57b7..c9fc3beb2db 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -41,16 +41,38 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "Deve ser uma lista de endere\u00e7os MAC separados por v\u00edrgulas" + "issues": { + "deprecate_smart_sensor": { + "description": "O sensor unificado de \"objeto detectado\" para detec\u00e7\u00f5es inteligentes agora est\u00e1 obsoleto. Ele foi substitu\u00eddo por sensores bin\u00e1rios de detec\u00e7\u00e3o inteligente individuais para cada tipo de detec\u00e7\u00e3o inteligente. Atualize quaisquer modelos ou automa\u00e7\u00f5es de acordo.", + "title": "Sensor de detec\u00e7\u00e3o inteligente obsoleto" }, + "ea_setup_failed": { + "description": "Voc\u00ea est\u00e1 usando v {version} do UniFi Protect, que \u00e9 uma vers\u00e3o de acesso antecipado. Ocorreu um erro irrecuper\u00e1vel ao tentar carregar a integra\u00e7\u00e3o. Fa\u00e7a [downgrade para uma vers\u00e3o est\u00e1vel](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) do UniFi Protect para continuar usando a integra\u00e7\u00e3o. \n\n Erro: {error}", + "title": "Erro de configura\u00e7\u00e3o usando a vers\u00e3o de acesso antecipado" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tem certeza de que deseja executar vers\u00f5es n\u00e3o suportadas do UniFi Protect? Isso pode fazer com que a integra\u00e7\u00e3o do Home Assistant seja interrompida.", + "title": "v {version} \u00e9 uma vers\u00e3o de acesso antecipado" + }, + "start": { + "description": "Voc\u00ea est\u00e1 usando v {version} do UniFi Protect, que \u00e9 uma vers\u00e3o de acesso antecipado. [Vers\u00f5es de acesso antecipado n\u00e3o s\u00e3o suportadas pelo Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) e \u00e9 recomend\u00e1vel voltar para uma vers\u00e3o est\u00e1vel assim que poss\u00edvel. \n\n Ao enviar este formul\u00e1rio, voc\u00ea fez o [downgrade do UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) ou concorda em executar uma vers\u00e3o n\u00e3o suportada do UniFi Protect.", + "title": "v {version} \u00e9 uma vers\u00e3o de acesso antecipado" + } + } + }, + "title": "UniFi Protect v {version} \u00e9 uma vers\u00e3o de acesso antecipado" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "M\u00e9tricas em tempo real (AVISO: aumenta muito o uso da CPU)", + "allow_ea": "Permitir vers\u00f5es de acesso antecipado do Protect (AVISO: marcar\u00e1 sua integra\u00e7\u00e3o como n\u00e3o suportada)", "disable_rtsp": "Desativar o fluxo RTSP", - "ignored_devices": "Lista separada por v\u00edrgulas de endere\u00e7os MAC de dispositivos a serem ignorados", "max_media": "N\u00famero m\u00e1ximo de eventos a serem carregados para o Media Browser (aumenta o uso de RAM)", "override_connection_host": "Anular o host de conex\u00e3o" }, diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index c1e5558c204..51e453aebb3 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -41,16 +41,38 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u043e\u0439 \u0441\u043f\u0438\u0441\u043e\u043a MAC-\u0430\u0434\u0440\u0435\u0441\u043e\u0432, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438." + "issues": { + "deprecate_smart_sensor": { + "description": "\u0421\u0435\u043d\u0441\u043e\u0440 \"Detected Object\" \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u043b. \u041e\u043d \u0437\u0430\u043c\u0435\u043d\u0451\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0438\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b.", + "title": "\u0421\u0435\u043d\u0441\u043e\u0440 Smart Detection \u0443\u0441\u0442\u0430\u0440\u0435\u043b" }, + "ea_setup_failed": { + "description": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 UniFi Protect v{version}, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u0435\u0439 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430. \u041f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, [\u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\n\n\u041e\u0448\u0438\u0431\u043a\u0430: {error}", + "title": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u0432\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 UniFi Protect? \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", + "title": "v{version} - \u0432\u0435\u0440\u0441\u0438\u044f \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "start": { + "description": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 UniFi Protect v{version}, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u0435\u0439 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430. [\u0412\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438.\n\n\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044f \u044d\u0442\u0443 \u0444\u043e\u0440\u043c\u0443, \u0412\u044b \u043b\u0438\u0431\u043e [\u043e\u0442\u043a\u0430\u0442\u0438\u043b\u0438\u0441\u044c \u043d\u0430 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect), \u043b\u0438\u0431\u043e \u0441\u043e\u0433\u043b\u0430\u0448\u0430\u0435\u0442\u0435\u0441\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e UniFi Protect.", + "title": "v{version} - \u0432\u0435\u0440\u0441\u0438\u044f \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + } + } + }, + "title": "UniFi Protect v{version} \u2014 \u044d\u0442\u043e \u0432\u0435\u0440\u0441\u0438\u044f \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438 (\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043d\u0430 \u0426\u041f)", + "allow_ea": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0432\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 UniFi Protect (\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0412\u0430\u0448\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043c\u0435\u0447\u0435\u043d\u0430 \u043a\u0430\u043a \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f)", "disable_rtsp": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0442\u043e\u043a RTSP", - "ignored_devices": "\u0421\u043f\u0438\u0441\u043e\u043a MAC-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c, \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e", "max_media": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0434\u043b\u044f \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043c\u0443\u043b\u044c\u0442\u0438\u043c\u0435\u0434\u0438\u0430 (\u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0439 \u043f\u0430\u043c\u044f\u0442\u0438)", "override_connection_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0443\u0437\u0435\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, diff --git a/homeassistant/components/unifiprotect/translations/sensor.en.json b/homeassistant/components/unifiprotect/translations/sensor.en.json new file mode 100644 index 00000000000..25d99e9ce4a --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/sensor.en.json @@ -0,0 +1,7 @@ +{ + "state": { + "unifiprotect__license_plate": { + "none": "Clear" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/sk.json b/homeassistant/components/unifiprotect/translations/sk.json index 3b59b1ff213..e84afc395eb 100644 --- a/homeassistant/components/unifiprotect/translations/sk.json +++ b/homeassistant/components/unifiprotect/translations/sk.json @@ -1,17 +1,43 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, + "flow_title": "{name} ({ip_address})", "step": { + "discovery_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, "reauth_confirm": { "data": { - "port": "Port" + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } }, "user": { "data": { - "port": "Port" + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Metriky v re\u00e1lnom \u010dase (UPOZORNENIE: V\u00fdrazne zvy\u0161uje vyu\u017eitie procesora)" } } } diff --git a/homeassistant/components/unifiprotect/translations/sv.json b/homeassistant/components/unifiprotect/translations/sv.json index 98b1862cc3d..37c3c792f15 100644 --- a/homeassistant/components/unifiprotect/translations/sv.json +++ b/homeassistant/components/unifiprotect/translations/sv.json @@ -42,15 +42,11 @@ } }, "options": { - "error": { - "invalid_mac_list": "M\u00e5ste vara en lista \u00f6ver MAC-adresser separerade med kommatecken" - }, "step": { "init": { "data": { "all_updates": "Realtidsm\u00e4tningar (VARNING: \u00d6kar CPU-anv\u00e4ndningen avsev\u00e4rt)", "disable_rtsp": "Inaktivera RTSP-str\u00f6mmen", - "ignored_devices": "Kommaseparerad lista \u00f6ver MAC-adresser f\u00f6r enheter att ignorera", "max_media": "Max antal h\u00e4ndelser som ska laddas f\u00f6r Media Browser (\u00f6kar RAM-anv\u00e4ndning)", "override_connection_host": "\u00c5sidos\u00e4tt anslutningsv\u00e4rd" }, diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index 65a8c52f368..d26f6af41ce 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -42,15 +42,11 @@ } }, "options": { - "error": { - "invalid_mac_list": "Virg\u00fclle ayr\u0131lm\u0131\u015f bir MAC adresleri listesi olmal\u0131d\u0131r" - }, "step": { "init": { "data": { "all_updates": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler (UYARI: CPU kullan\u0131m\u0131n\u0131 b\u00fcy\u00fck \u00f6l\u00e7\u00fcde art\u0131r\u0131r)", "disable_rtsp": "RTSP ak\u0131\u015f\u0131n\u0131 devre d\u0131\u015f\u0131 b\u0131rak\u0131n", - "ignored_devices": "Yok say\u0131lacak ayg\u0131tlar\u0131n MAC adreslerinin virg\u00fclle ayr\u0131lm\u0131\u015f listesi", "max_media": "Medya Taray\u0131c\u0131 i\u00e7in y\u00fcklenecek maksimum olay say\u0131s\u0131 (RAM kullan\u0131m\u0131n\u0131 art\u0131r\u0131r)", "override_connection_host": "Ba\u011flant\u0131 Ana Bilgisayar\u0131n\u0131 Ge\u00e7ersiz K\u0131l" }, diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index 0688a40d0c8..ca8f1297d25 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -41,16 +41,38 @@ } } }, - "options": { - "error": { - "invalid_mac_list": "\u5fc5\u9808\u70ba\u4ee5\u9017\u865f\uff08\uff1a\uff09\u5206\u9694\u958b\u7684 MAC \u5730\u5740\u5217\u8868" + "issues": { + "deprecate_smart_sensor": { + "description": "\u7528\u65bc\u667a\u80fd\u5075\u6e2c\u7684\u7d71\u4e00 \"\u5075\u6e2c\u7269\u4ef6\" \u611f\u6e2c\u5668\u5df2\u68c4\u7528\uff0c\u6539\u4ee5\u500b\u5225\u667a\u80fd\u5075\u6e2c\u985e\u578b\u7684\u667a\u80fd\u5075\u6e2c\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u4f86\u53d6\u4ee3\u3002\u8acb\u66f4\u65b0\u76f8\u5c0d\u61c9\u7684\u6a23\u677f\u6216\u81ea\u52d5\u5316\u3002", + "title": "\u667a\u80fd\u5075\u6e2c\u611f\u6e2c\u5668\u5df2\u505c\u7528" }, + "ea_setup_failed": { + "description": "\u6b63\u5728\u4f7f\u7528\u7684 UniFi Protect v{version} \u7248\u6436\u5148\u9ad4\u9a57\u7248\u3002\u5617\u8a66\u8f09\u5165\u6574\u5408\u6642\u767c\u751f\u4e0d\u53ef\u6062\u5fa9\u7684\u932f\u8aa4\u3002\u8acb[\u964d\u7d1a\u81f3\u7a69\u5b9a\u7248\u672c](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) \u4e4b UniFi Protect \u4ee5\u7e7c\u7e8c\u4f7f\u7528\u6b64\u6574\u5408\u3002\n\n\u932f\u8aa4\uff1a{error}", + "title": "\u4f7f\u7528\u6436\u5148\u9ad4\u9a57\u7248\u8a2d\u5b9a\u932f\u8aa4" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u78ba\u5b9a\u8981\u4f7f\u7528\u4e0d\u652f\u63f4\u7684 UniFi Protect \u7248\u672c\uff1f\u53ef\u80fd\u6703\u5c0e\u81f4 Home Assistant \u6574\u5408\u4e0d\u7a69\u5b9a\u3002", + "title": "v{version} \u7248\u70ba\u6436\u5148\u9ad4\u9a57\u7248" + }, + "start": { + "description": "\u6b63\u5728\u4f7f\u7528\u7684 UniFi Protect v{version} \u7248\u6436\u5148\u9ad4\u9a57\u7248\u3002 [Home Assistant \u4e0d\u652f\u63f4\u6436\u5148\u9ad4\u9a57\u7248] (https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access)\u3001\u4e26\u5efa\u8b70\u76e1\u53ef\u80fd\u9000\u56de\u81f3\u7a69\u5b9a\u7248\u672c\u3002\n\n\u50b3\u9001\u6b64\u8868\u683c\u8868\u793a [\u5df2\u964d\u7d1a UniFi Protect \u7248\u672c] (https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) \u6216\u540c\u610f\u4f7f\u7528\u4e0d\u652f\u63f4\u7684 UniFi Protect \u7248\u672c\u3002", + "title": "v{version} \u7248\u70ba\u6436\u5148\u9ad4\u9a57\u7248" + } + } + }, + "title": "UniFi Protect {version} \u7248\u70ba\u6436\u5148\u9ad4\u9a57\u7248" + } + }, + "options": { "step": { "init": { "data": { "all_updates": "\u5373\u6642\u6307\u6a19\uff08\u8b66\u544a\uff1a\u5927\u91cf\u63d0\u5347 CPU \u4f7f\u7528\u7387\uff09", + "allow_ea": "\u5141\u8a31\u4f7f\u7528\u6436\u5148\u9ad4\u9a57\u7248 Protect\uff08\u8b66\u544a\uff1a\u53ef\u80fd\u6703\u5c0e\u81f4\u6574\u5408\u8b8a\u70ba\u4e0d\u652f\u63f4\uff09)", "disable_rtsp": "\u95dc\u9589 RTSP \u4e32\u6d41", - "ignored_devices": "\u4ee5\u9017\u865f\u5206\u9694\u7684\u5ffd\u7565 MAC \u4f4d\u5740\u5217\u8868", "max_media": "\u5a92\u9ad4\u700f\u89bd\u5668\u6700\u9ad8\u8f09\u5165\u4e8b\u4ef6\u6578\uff08\u589e\u52a0\u8a18\u61b6\u9ad4\u4f7f\u7528\uff09", "override_connection_host": "\u7f6e\u63db\u9023\u7dda\u4e3b\u6a5f\u7aef" }, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 808117aac9e..f58bb14eb41 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -7,6 +7,8 @@ from enum import Enum import socket from typing import Any +from aiohttp import CookieJar +from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( Bootstrap, Light, @@ -16,9 +18,23 @@ from pyunifiprotect.data import ( ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import DOMAIN, ModelType +from .const import ( + CONF_ALL_UPDATES, + CONF_OVERRIDE_CHOST, + DEVICES_FOR_SUBSCRIBE, + DOMAIN, + ModelType, +) def get_nested_attr(obj: Any, attr: str) -> Any: @@ -106,3 +122,24 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: """Generate entry specific dispatch ID.""" return f"{DOMAIN}.{entry.entry_id}.{dispatch}" + + +@callback +def async_create_api_client( + hass: HomeAssistant, entry: ConfigEntry +) -> ProtectApiClient: + """Create ProtectApiClient from config entry.""" + + session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + return ProtectApiClient( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + verify_ssl=entry.data[CONF_VERIFY_SSL], + session=session, + subscribed_models=DEVICES_FOR_SUBSCRIBE, + override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), + ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), + ignore_unadopted=False, + ) diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index a8a767c8879..e05dcde1751 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -8,11 +8,12 @@ from typing import Any from urllib.parse import urlencode from aiohttp import web -from pyunifiprotect.data import Event +from pyunifiprotect.data import Camera, Event from pyunifiprotect.exceptions import ClientError from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN from .data import ProtectData @@ -104,8 +105,10 @@ class ProtectProxyView(HomeAssistantView): def _get_data_or_404(self, nvr_id: str) -> ProtectData | web.Response: all_data: list[ProtectData] = [] - for data in self.data.values(): + for entry_id, data in self.data.items(): if isinstance(data, ProtectData): + if nvr_id == entry_id: + return data if data.api.bootstrap.nvr.id == nvr_id: return data all_data.append(data) @@ -160,6 +163,27 @@ class VideoProxyView(ProtectProxyView): url = "/api/unifiprotect/video/{nvr_id}/{camera_id}/{start}/{end}" name = "api:unifiprotect_thumbnail" + @callback + def _async_get_camera(self, data: ProtectData, camera_id: str) -> Camera | None: + if (camera := data.api.bootstrap.cameras.get(camera_id)) is not None: + return camera + + entity_registry = er.async_get(self.hass) + device_registry = dr.async_get(self.hass) + + if (entity := entity_registry.async_get(camera_id)) is None or ( + device := device_registry.async_get(entity.device_id or "") + ) is None: + return None + + macs = [c[1] for c in device.connections if c[0] == dr.CONNECTION_NETWORK_MAC] + for mac in macs: + if (ufp_device := data.api.bootstrap.get_device_from_mac(mac)) is not None: + if isinstance(ufp_device, Camera): + camera = ufp_device + break + return camera + async def get( self, request: web.Request, nvr_id: str, camera_id: str, start: str, end: str ) -> web.StreamResponse: @@ -169,7 +193,7 @@ class VideoProxyView(ProtectProxyView): if isinstance(data, web.Response): return data - camera = data.api.bootstrap.cameras.get(camera_id) + camera = self._async_get_camera(data, camera_id) if camera is None: return _404(f"Invalid camera ID: {camera_id}") if not camera.can_read_media(data.api.bootstrap.auth_user): diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index d26b062de40..3c6ba701f7d 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -453,9 +453,11 @@ class UniversalMediaPlayer(MediaPlayerEntity): return self._override_or_child_attr(ATTR_MEDIA_SHUFFLE) @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - flags = self._child_attr(ATTR_SUPPORTED_FEATURES) or 0 + flags: MediaPlayerEntityFeature = self._child_attr( + ATTR_SUPPORTED_FEATURES + ) or MediaPlayerEntityFeature(0) if SERVICE_TURN_ON in self._cmds: flags |= MediaPlayerEntityFeature.TURN_ON diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 85cda1f0061..47680714d19 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -71,7 +71,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): return {self.color_mode} @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag supported features.""" if self._element.dimmable: return LightEntityFeature.TRANSITION | LightEntityFeature.FLASH diff --git a/homeassistant/components/upb/translations/sk.json b/homeassistant/components/upb/translations/sk.json index 3f20d345b26..2174735cca1 100644 --- a/homeassistant/components/upb/translations/sk.json +++ b/homeassistant/components/upb/translations/sk.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "address": "Adresa (pozri popis vy\u0161\u0161ie)", + "protocol": "Protokol" + }, + "title": "Pripojte sa k UPB PIM" + } } } } \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/id.json b/homeassistant/components/upcloud/translations/id.json index 4ff6a8c7d92..d51e6a2e712 100644 --- a/homeassistant/components/upcloud/translations/id.json +++ b/homeassistant/components/upcloud/translations/id.json @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "scan_interval": "Interval pembaruan (dalam detik, minimal 30)" + "scan_interval": "Interval pembaruan dalam detik, minimal 30" } } } diff --git a/homeassistant/components/upcloud/translations/sk.json b/homeassistant/components/upcloud/translations/sk.json index 5ada995aa6e..5cfcde03474 100644 --- a/homeassistant/components/upcloud/translations/sk.json +++ b/homeassistant/components/upcloud/translations/sk.json @@ -1,7 +1,25 @@ { "config": { "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval aktualiz\u00e1cie v sekund\u00e1ch, minim\u00e1lne 30" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 9ec748c1ed5..57e20a764ba 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -190,7 +190,7 @@ class UpdateEntity(RestoreEntity): _attr_release_summary: str | None = None _attr_release_url: str | None = None _attr_state: None = None - _attr_supported_features: int = 0 + _attr_supported_features: UpdateEntityFeature = UpdateEntityFeature(0) _attr_title: str | None = None __skipped_version: str | None = None __in_progress: bool = False @@ -270,7 +270,7 @@ class UpdateEntity(RestoreEntity): return self._attr_release_url @property - def supported_features(self) -> int: + def supported_features(self) -> UpdateEntityFeature: """Flag supported features.""" return self._attr_supported_features diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py index aec3c183a63..c9340473298 100644 --- a/homeassistant/components/update/const.py +++ b/homeassistant/components/update/const.py @@ -1,13 +1,13 @@ """Constants for the update component.""" from __future__ import annotations -from enum import IntEnum +from enum import IntFlag from typing import Final DOMAIN: Final = "update" -class UpdateEntityFeature(IntEnum): +class UpdateEntityFeature(IntFlag): """Supported features of the update entity.""" INSTALL = 1 diff --git a/homeassistant/components/update/translations/sk.json b/homeassistant/components/update/translations/sk.json new file mode 100644 index 00000000000..76ac84624ef --- /dev/null +++ b/homeassistant/components/update/translations/sk.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "trigger_type": { + "changed_states": "Dostupnos\u0165 aktualiz\u00e1cie {entity_name} sa zmenila", + "turned_off": "{entity_name} sa stalo aktu\u00e1lnym" + } + }, + "title": "Aktualizova\u0165" +} \ No newline at end of file diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 4c45b099193..214f5ce2931 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.32.3", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json index a8501a7de41..36640a492a9 100644 --- a/homeassistant/components/upnp/translations/he.json +++ b/homeassistant/components/upnp/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/upnp/translations/hr.json b/homeassistant/components/upnp/translations/hr.json index 941f72f2e7d..785e860e69b 100644 --- a/homeassistant/components/upnp/translations/hr.json +++ b/homeassistant/components/upnp/translations/hr.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei" + }, "error": { "few": "Nekoliko", "one": "Jedan", "other": "Ostalo" + }, + "step": { + "init": { + "few": "Nekoliko", + "one": "Jedan", + "other": "Ostalo" + } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/sk.json b/homeassistant/components/upnp/translations/sk.json index 793f8eff278..ae6b0093d7f 100644 --- a/homeassistant/components/upnp/translations/sk.json +++ b/homeassistant/components/upnp/translations/sk.json @@ -1,7 +1,16 @@ { "config": { "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "unique_id": "Zariadenie" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/bg.json b/homeassistant/components/uptime/translations/bg.json index 1290144ec04..8a6bd7e5072 100644 --- a/homeassistant/components/uptime/translations/bg.json +++ b/homeassistant/components/uptime/translations/bg.json @@ -8,5 +8,11 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" } } + }, + "issues": { + "removed_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Uptime \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Uptime \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/sk.json b/homeassistant/components/uptime/translations/sk.json new file mode 100644 index 00000000000..6ba11236f08 --- /dev/null +++ b/homeassistant/components/uptime/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/bg.json b/homeassistant/components/uptimerobot/translations/bg.json index d75409d8ee1..d729a88dbc0 100644 --- a/homeassistant/components/uptimerobot/translations/bg.json +++ b/homeassistant/components/uptimerobot/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/uptimerobot/translations/sensor.sk.json b/homeassistant/components/uptimerobot/translations/sensor.sk.json new file mode 100644 index 00000000000..4177567e40e --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "not_checked_yet": "Zatia\u013e neskontrolovan\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sk.json b/homeassistant/components/uptimerobot/translations/sk.json index a41b646034b..e9d14e025a2 100644 --- a/homeassistant/components/uptimerobot/translations/sk.json +++ b/homeassistant/components/uptimerobot/translations/sk.json @@ -1,16 +1,22 @@ { "config": { "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { - "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", + "not_main_key": "Bol zisten\u00fd nespr\u00e1vny typ k\u013e\u00fa\u010da API, pou\u017eite \u201emain\u201c k\u013e\u00fa\u010d API", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "reauth_confirm": { "data": { "api_key": "API k\u013e\u00fa\u010d" - } + }, + "title": "Znova overi\u0165 integr\u00e1ciu" }, "user": { "data": { diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 7c0355fa24c..0f81d2e42d6 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -64,6 +64,24 @@ def async_register_scan_request_callback( @hass_callback def async_is_plugged_in(hass: HomeAssistant, matcher: USBCallbackMatcher) -> bool: """Return True is a USB device is present.""" + + vid = matcher.get("vid", "") + pid = matcher.get("pid", "") + serial_number = matcher.get("serial_number", "") + manufacturer = matcher.get("manufacturer", "") + description = matcher.get("description", "") + + if ( + vid != vid.upper() + or pid != pid.upper() + or serial_number != serial_number.lower() + or manufacturer != manufacturer.lower() + or description != description.lower() + ): + raise ValueError( + f"vid and pid must be uppercase, the rest lowercase in matcher {matcher!r}" + ) + usb_discovery: USBDiscovery = hass.data[DOMAIN] return any( _is_matching(USBDevice(*device_tuple), matcher) diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index bd8ec9633bd..ee37381a6fa 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aio_geojson_usgs_earthquakes==0.1"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["aio_geojson_usgs_earthquakes"] + "loggers": ["aio_geojson_usgs_earthquakes"], + "integration_type": "service" } diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index cabd90ba8bd..5424d8a55ad 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -6,13 +6,14 @@ from typing import Any, cast import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, SchemaConfigFlowHandler, SchemaFlowError, SchemaFlowFormStep, - SchemaFlowMenuStep, ) from .const import ( @@ -46,20 +47,22 @@ METER_TYPES = [ ] -def _validate_config(data: Any) -> Any: +async def _validate_config( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: """Validate config.""" try: - vol.Unique()(data[CONF_TARIFFS]) + vol.Unique()(user_input[CONF_TARIFFS]) except vol.Invalid as exc: raise SchemaFlowError("tariffs_not_unique") from exc - return data + return user_input OPTIONS_SCHEMA = vol.Schema( { vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector( - selector.EntitySelectorConfig(domain="sensor"), + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN), ), } ) @@ -68,7 +71,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): selector.TextSelector(), vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector( - selector.EntitySelectorConfig(domain="sensor"), + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN), ), vol.Required(CONF_METER_TYPE): selector.SelectSelector( selector.SelectSelectorConfig(options=METER_TYPES), @@ -93,12 +96,12 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { +CONFIG_FLOW = { "user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_config) } -OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), } diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index 194eff5d7d0..4252f796199 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -13,6 +13,7 @@ calibrate: target: entity: domain: sensor + integration: utility_meter fields: value: name: Value diff --git a/homeassistant/components/utility_meter/translations/ca.json b/homeassistant/components/utility_meter/translations/ca.json index 6781fc9533c..d48052eaf33 100644 --- a/homeassistant/components/utility_meter/translations/ca.json +++ b/homeassistant/components/utility_meter/translations/ca.json @@ -12,7 +12,7 @@ "tariffs": "Tarifes suportades" }, "data_description": { - "delta_values": "Activa-ho si els les lectures s\u00f3n valors delta (actual - anterior) en lloc de valors absoluts.", + "delta_values": "Activa-ho si les lectures s\u00f3n valors delta des de l'\u00faltima lectura en lloc de valors absoluts.", "net_consumption": "Activa-ho si \u00e9s un comptador net, \u00e9s a dir, pot augmentar i disminuir.", "offset": "Despla\u00e7a el dia de restabliment mensual del comptador.", "tariffs": "Llista de tarifes admeses, deixa-la en blanc si utilitzes una \u00fanica tarifa." diff --git a/homeassistant/components/utility_meter/translations/de.json b/homeassistant/components/utility_meter/translations/de.json index 870772e0dbe..798d5616edb 100644 --- a/homeassistant/components/utility_meter/translations/de.json +++ b/homeassistant/components/utility_meter/translations/de.json @@ -17,7 +17,7 @@ "offset": "Versetzen des Tages einer monatlichen Z\u00e4hlerr\u00fccksetzung.", "tariffs": "Eine Liste der unterst\u00fctzten Tarife; leer lassen, wenn nur ein einziger Tarif ben\u00f6tigt wird." }, - "description": "Erstelle einen Sensor, der den Verbrauch verschiedener Versorgungsleistungen (z. B. Energie, Gas, Wasser, Heizung) \u00fcber einen konfigurierten Zeitraum, in der Regel monatlich, erfasst. Der Sensor f\u00fcr den Verbrauchsz\u00e4hler unterst\u00fctzt optional die Aufteilung des Verbrauchs nach Tarifen. In diesem Fall wird ein Sensor f\u00fcr jeden Tarif sowie eine Auswahlm\u00f6glichkeit zur Auswahl des aktuellen Tarifs erstellt.", + "description": "Erstelle einen Sensor, der den Verbrauch verschiedener Versorgungsleistungen (z.B. Energie, Gas, Wasser, Heizung) \u00fcber einen konfigurierten Zeitraum, in der Regel monatlich, erfasst. Der Sensor f\u00fcr den Verbrauchsz\u00e4hler unterst\u00fctzt optional die Aufteilung des Verbrauchs nach Tarifen. In diesem Fall wird ein Sensor f\u00fcr jeden Tarif sowie eine Auswahlm\u00f6glichkeit zur Auswahl des aktuellen Tarifs erstellt.", "title": "Verbrauchsz\u00e4hler hinzuf\u00fcgen" } } diff --git a/homeassistant/components/simplisafe/translations/fi.json b/homeassistant/components/utility_meter/translations/hr.json similarity index 55% rename from homeassistant/components/simplisafe/translations/fi.json rename to homeassistant/components/utility_meter/translations/hr.json index 47a52e9a0c6..d4e640e4069 100644 --- a/homeassistant/components/simplisafe/translations/fi.json +++ b/homeassistant/components/utility_meter/translations/hr.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "password": "Salasana", - "username": "S\u00e4hk\u00f6postiosoite" + "name": "Ime" } } } diff --git a/homeassistant/components/utility_meter/translations/id.json b/homeassistant/components/utility_meter/translations/id.json index d2f9bab72c5..4375e6f9764 100644 --- a/homeassistant/components/utility_meter/translations/id.json +++ b/homeassistant/components/utility_meter/translations/id.json @@ -26,7 +26,7 @@ "step": { "init": { "data": { - "source": "Sensor Input" + "source": "Sensor input" } } } diff --git a/homeassistant/components/utility_meter/translations/sk.json b/homeassistant/components/utility_meter/translations/sk.json new file mode 100644 index 00000000000..54c043ce6e2 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "source": "Vstupn\u00fd sn\u00edma\u010d", + "tariffs": "Podporovan\u00e9 tarify" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Vstupn\u00fd sn\u00edma\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index c64642077da..2417908ecec 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -107,14 +107,14 @@ class UnifiVideoCamera(Camera): return self._name @property - def supported_features(self) -> int: + def supported_features(self) -> CameraEntityFeature: """Return supported features.""" channels = self._caminfo["channels"] for channel in channels: if channel["isRtspEnabled"]: return CameraEntityFeature.STREAM - return 0 + return CameraEntityFeature(0) @property def extra_state_attributes(self): diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 2942078a875..cf82836cbec 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta -from enum import IntEnum +from enum import IntFlag from functools import partial import logging from typing import Any, final @@ -74,7 +74,7 @@ STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR] DEFAULT_NAME = "Vacuum cleaner robot" -class VacuumEntityFeature(IntEnum): +class VacuumEntityFeature(IntFlag): """Supported features of the vacuum entity.""" TURN_ON = 1 @@ -180,10 +180,10 @@ class _BaseVacuum(Entity): _attr_battery_level: int | None = None _attr_fan_speed: str | None = None _attr_fan_speed_list: list[str] - _attr_supported_features: int + _attr_supported_features: VacuumEntityFeature = VacuumEntityFeature(0) @property - def supported_features(self) -> int: + def supported_features(self) -> VacuumEntityFeature: """Flag vacuum cleaner features that are supported.""" return self._attr_supported_features @@ -318,11 +318,12 @@ class VacuumEntity(_BaseVacuum, ToggleEntity): """Representation of a vacuum cleaner robot.""" entity_description: VacuumEntityDescription + _attr_status: str | None = None @property def status(self) -> str | None: """Return the status of the vacuum cleaner.""" - return None + return self._attr_status @property def battery_icon(self) -> str: @@ -394,11 +395,12 @@ class StateVacuumEntity(_BaseVacuum): """Representation of a vacuum cleaner robot that supports states.""" entity_description: StateVacuumEntityDescription + _attr_state: str | None = None @property def state(self) -> str | None: """Return the state of the vacuum cleaner.""" - return None + return self._attr_state @property def battery_icon(self) -> str: diff --git a/homeassistant/components/vacuum/translations/de.json b/homeassistant/components/vacuum/translations/de.json index 8de386b3506..cb9a1ef5d83 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": "Unt\u00e4tig", + "idle": "Inaktiv", "off": "Aus", "on": "An", "paused": "Pausiert", diff --git a/homeassistant/components/vacuum/translations/is.json b/homeassistant/components/vacuum/translations/is.json index 759191ae67e..da930604165 100644 --- a/homeassistant/components/vacuum/translations/is.json +++ b/homeassistant/components/vacuum/translations/is.json @@ -1,4 +1,9 @@ { + "device_automation": { + "condition_type": { + "is_cleaning": "{entity_name} er a\u00f0 \u00fer\u00edfa" + } + }, "state": { "_": { "cleaning": "A\u00f0 ryksuga", diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 6a2234af9e6..f393342dfd5 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -63,6 +63,8 @@ PLATFORMS: list[str] = [ Platform.SENSOR, Platform.FAN, Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SWITCH, ] ATTR_PROFILE_FAN_SPEED = "fan_speed" diff --git a/homeassistant/components/vallox/number.py b/homeassistant/components/vallox/number.py new file mode 100644 index 00000000000..5be91fe66e6 --- /dev/null +++ b/homeassistant/components/vallox/number.py @@ -0,0 +1,127 @@ +"""Support for Vallox ventilation unit numbers.""" +from __future__ import annotations + +from dataclasses import dataclass + +from vallox_websocket_api import Vallox + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ValloxDataUpdateCoordinator, ValloxEntity +from .const import DOMAIN + + +class ValloxNumberEntity(ValloxEntity, NumberEntity): + """Representation of a Vallox number entity.""" + + entity_description: ValloxNumberEntityDescription + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + name: str, + coordinator: ValloxDataUpdateCoordinator, + description: ValloxNumberEntityDescription, + client: Vallox, + ) -> None: + """Initialize the Vallox number entity.""" + super().__init__(name, coordinator) + + self.entity_description = description + + self._attr_unique_id = f"{self._device_uuid}-{description.key}" + self._client = client + + @property + def native_value(self) -> float | None: + """Return the value reported by the sensor.""" + if ( + value := self.coordinator.data.get_metric( + self.entity_description.metric_key + ) + ) is None: + return None + + return float(value) + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self._client.set_values( + {self.entity_description.metric_key: float(value)} + ) + await self.coordinator.async_request_refresh() + + +@dataclass +class ValloxMetricMixin: + """Holds Vallox metric key.""" + + metric_key: str + + +@dataclass +class ValloxNumberEntityDescription(NumberEntityDescription, ValloxMetricMixin): + """Describes Vallox number entity.""" + + +NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( + ValloxNumberEntityDescription( + key="supply_air_target_home", + name="Supply air temperature (Home)", + metric_key="A_CYC_HOME_AIR_TEMP_TARGET", + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + native_min_value=5.0, + native_max_value=25.0, + native_step=1.0, + ), + ValloxNumberEntityDescription( + key="supply_air_target_away", + name="Supply air temperature (Away)", + metric_key="A_CYC_AWAY_AIR_TEMP_TARGET", + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + native_min_value=5.0, + native_max_value=25.0, + native_step=1.0, + ), + ValloxNumberEntityDescription( + key="supply_air_target_boost", + name="Supply air temperature (Boost)", + metric_key="A_CYC_BOOST_AIR_TEMP_TARGET", + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + native_min_value=5.0, + native_max_value=25.0, + native_step=1.0, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensors.""" + data = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [ + ValloxNumberEntity( + data["name"], data["coordinator"], description, data["client"] + ) + for description in NUMBER_ENTITIES + ] + ) diff --git a/homeassistant/components/vallox/switch.py b/homeassistant/components/vallox/switch.py new file mode 100644 index 00000000000..4c8116ebd27 --- /dev/null +++ b/homeassistant/components/vallox/switch.py @@ -0,0 +1,105 @@ +"""Support for Vallox ventilation unit switches.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from vallox_websocket_api import Vallox + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ValloxDataUpdateCoordinator, ValloxEntity +from .const import DOMAIN + + +class ValloxSwitchEntity(ValloxEntity, SwitchEntity): + """Representation of a Vallox switch.""" + + entity_description: ValloxSwitchEntityDescription + _attr_entity_category = EntityCategory.CONFIG + _attr_has_entity_name = True + + def __init__( + self, + name: str, + coordinator: ValloxDataUpdateCoordinator, + description: ValloxSwitchEntityDescription, + client: Vallox, + ) -> None: + """Initialize the Vallox switch.""" + super().__init__(name, coordinator) + + self.entity_description = description + + self._attr_unique_id = f"{self._device_uuid}-{description.key}" + self._client = client + + @property + def is_on(self) -> bool | None: + """Return true if the switch is on.""" + if ( + value := self.coordinator.data.get_metric( + self.entity_description.metric_key + ) + ) is None: + return None + return value == 1 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on.""" + await self._set_value(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off.""" + await self._set_value(False) + + async def _set_value(self, value: bool) -> None: + """Update the current value.""" + metric_key = self.entity_description.metric_key + await self._client.set_values({metric_key: 1 if value else 0}) + await self.coordinator.async_request_refresh() + + +@dataclass +class ValloxMetricKeyMixin: + """Dataclass to allow defining metric_key without a default value.""" + + metric_key: str + + +@dataclass +class ValloxSwitchEntityDescription(SwitchEntityDescription, ValloxMetricKeyMixin): + """Describes Vallox switch entity.""" + + +SWITCH_ENTITIES: tuple[ValloxSwitchEntityDescription, ...] = ( + ValloxSwitchEntityDescription( + key="bypass_locked", + name="Bypass locked", + icon="mdi:arrow-horizontal-lock", + metric_key="A_CYC_BYPASS_LOCKED", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the switches.""" + + data = hass.data[DOMAIN][entry.entry_id] + client = data["client"] + client.set_settable_address("A_CYC_BYPASS_LOCKED", int) + + async_add_entities( + [ + ValloxSwitchEntity(data["name"], data["coordinator"], description, client) + for description in SWITCH_ENTITIES + ] + ) diff --git a/homeassistant/components/vallox/translations/ru.json b/homeassistant/components/vallox/translations/ru.json index a1165b287d1..990357a015e 100644 --- a/homeassistant/components/vallox/translations/ru.json +++ b/homeassistant/components/vallox/translations/ru.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \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_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\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." }, "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_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\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." }, "step": { diff --git a/homeassistant/components/vallox/translations/sk.json b/homeassistant/components/vallox/translations/sk.json new file mode 100644 index 00000000000..9a6fb3929c5 --- /dev/null +++ b/homeassistant/components/vallox/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 2ac751e283d..0da64227fd3 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -139,7 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle a clear cache service call.""" # clear the cache with suppress(FileNotFoundError): - if call.data[CONF_ADDRESS]: + if CONF_ADDRESS in call.data and call.data[CONF_ADDRESS]: await hass.async_add_executor_job( os.unlink, hass.config.path( diff --git a/homeassistant/components/velbus/translations/sk.json b/homeassistant/components/velbus/translations/sk.json new file mode 100644 index 00000000000..6e5e2436adf --- /dev/null +++ b/homeassistant/components/velbus/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "port": "Re\u0165azec pripojenia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index f721a628ef8..c924fe5c10b 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -40,7 +40,7 @@ class VeluxCover(VeluxEntity, CoverEntity): """Representation of a Velux cover.""" @property - def supported_features(self) -> int: + def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" supported_features = ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 2fb0595788f..2a921fe3731 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -125,7 +125,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): self._attr_name = self._client.name @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = ( ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/venstar/translations/sk.json b/homeassistant/components/venstar/translations/sk.json new file mode 100644 index 00000000000..15cffb46878 --- /dev/null +++ b/homeassistant/components/venstar/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "pin": "PIN k\u00f3d", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripojte k termostatu Venstar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 8f541bf21b4..9f488fad33e 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -65,7 +65,7 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], SensorEntity): return self.current_value @property - def device_class(self) -> str | None: + def device_class(self) -> SensorDeviceClass | None: """Return the class of this entity.""" if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return SensorDeviceClass.TEMPERATURE diff --git a/homeassistant/components/vera/translations/sk.json b/homeassistant/components/vera/translations/sk.json new file mode 100644 index 00000000000..23f69af3665 --- /dev/null +++ b/homeassistant/components/vera/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepodarilo sa pripoji\u0165 k kontrol\u00e9ru s adresou URL {base_url}" + }, + "step": { + "user": { + "data": { + "vera_controller_url": "URL kontrol\u00e9ra" + }, + "data_description": { + "vera_controller_url": "Malo by to vyzera\u0165 takto: http://192.168.1.161:3480" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/bg.json b/homeassistant/components/verisure/translations/bg.json index 927c79f2674..6411c970f11 100644 --- a/homeassistant/components/verisure/translations/bg.json +++ b/homeassistant/components/verisure/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/verisure/translations/de.json b/homeassistant/components/verisure/translations/de.json index b5e544e579b..d306064d121 100644 --- a/homeassistant/components/verisure/translations/de.json +++ b/homeassistant/components/verisure/translations/de.json @@ -14,7 +14,7 @@ "data": { "giid": "Installation" }, - "description": "Home Assistant hat mehrere Verisure-Installationen in deinen My Pages-Konto gefunden. Bitte w\u00e4hle die Installation aus, die du zu Home Assistant hinzuf\u00fcgen m\u00f6chtest." + "description": "Home Assistant hat mehrere Verisure-Installationen in deinem My Pages-Konto gefunden. Bitte w\u00e4hle die Installation aus, die du zu Home Assistant hinzuf\u00fcgen m\u00f6chtest." }, "mfa": { "data": { @@ -52,7 +52,7 @@ "init": { "data": { "lock_code_digits": "Anzahl der Ziffern im PIN-Code f\u00fcr Schl\u00f6sser", - "lock_default_code": "Standard-PIN-Code f\u00fcr Schl\u00f6sser, wird verwendet wenn keiner angegeben wird" + "lock_default_code": "Standard-PIN-Code f\u00fcr Schl\u00f6sser, wird verwendet, wenn keiner angegeben wird" } } } diff --git a/homeassistant/components/verisure/translations/sk.json b/homeassistant/components/verisure/translations/sk.json index 0f898b977ee..33dc3546d09 100644 --- a/homeassistant/components/verisure/translations/sk.json +++ b/homeassistant/components/verisure/translations/sk.json @@ -1,20 +1,38 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { + "installation": { + "data": { + "giid": "In\u0161tal\u00e1cia" + } + }, + "mfa": { + "data": { + "code": "Overovac\u00ed k\u00f3d" + } + }, "reauth_confirm": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" + } + }, + "reauth_mfa": { + "data": { + "code": "Overovac\u00ed k\u00f3d" } }, "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Heslo" } } } diff --git a/homeassistant/components/version/translations/sk.json b/homeassistant/components/version/translations/sk.json new file mode 100644 index 00000000000..8e621c87efb --- /dev/null +++ b/homeassistant/components/version/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "version_source": "Zdroj verzie" + }, + "title": "Vyberte typ in\u0161tal\u00e1cie" + }, + "version_source": { + "data": { + "beta": "Zahrn\u00fa\u0165 beta verzie" + }, + "title": "Konfigurova\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 49be473b748..730da2d586b 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==2.0.3"], + "requirements": ["pyvesync==2.1.1"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pyvesync"] diff --git a/homeassistant/components/vesync/translations/sk.json b/homeassistant/components/vesync/translations/sk.json index c043ef9ff19..4a87b488c17 100644 --- a/homeassistant/components/vesync/translations/sk.json +++ b/homeassistant/components/vesync/translations/sk.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "user": { "data": { + "password": "Heslo", "username": "Email" - } + }, + "title": "Zadajte pou\u017e\u00edvate\u013esk\u00e9 meno a heslo" } } } diff --git a/homeassistant/components/vicare/translations/sk.json b/homeassistant/components/vicare/translations/sk.json index 1a2c2a47260..a2bbd24a57e 100644 --- a/homeassistant/components/vicare/translations/sk.json +++ b/homeassistant/components/vicare/translations/sk.json @@ -1,14 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "client_id": "API k\u013e\u00fa\u010d", + "heating_type": "Typ vykurovania", + "password": "Heslo", "username": "Email" - } + }, + "description": "Nastavte integr\u00e1ciu ViCare. Ak chcete vygenerova\u0165 k\u013e\u00fa\u010d API, prejdite na https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vilfo/translations/sk.json b/homeassistant/components/vilfo/translations/sk.json index 7afa1eaea6e..cb89b70e26e 100644 --- a/homeassistant/components/vilfo/translations/sk.json +++ b/homeassistant/components/vilfo/translations/sk.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { - "access_token": "Pr\u00edstupov\u00fd token" + "access_token": "Pr\u00edstupov\u00fd token", + "host": "Hostite\u013e" } } } diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index a47c2e0f036..5d8049583de 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst.", + "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass die eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst.", "existing_config_entry_found": "Ein bestehender VIZIO SmartCast-Ger\u00e4t Config-Eintrag mit der gleichen Seriennummer wurde bereits konfiguriert. Du musst den vorhandenen Eintrag l\u00f6schen, um diesen zu konfigurieren." }, "step": { diff --git a/homeassistant/components/vizio/translations/sk.json b/homeassistant/components/vizio/translations/sk.json index 171a5a2c708..b43555b8eec 100644 --- a/homeassistant/components/vizio/translations/sk.json +++ b/homeassistant/components/vizio/translations/sk.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "already_configured_device": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { + "pair_tv": { + "data": { + "pin": "PIN k\u00f3d" + } + }, "user": { "data": { "access_token": "Pr\u00edstupov\u00fd token", + "device_class": "Typ zariadenia", + "host": "Hostite\u013e", "name": "N\u00e1zov" } } diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index a2f13de179d..9f28a60427c 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -67,69 +67,28 @@ class VlcDevice(MediaPlayerEntity): """Initialize the vlc device.""" self._instance = vlc.Instance(arguments) self._vlc = self._instance.media_player_new() - self._name = name - self._volume = None - self._muted = None - self._state = None - self._media_position_updated_at = None - self._media_position = None - self._media_duration = None + self._attr_name = name def update(self): """Get the latest details from the device.""" status = self._vlc.get_state() if status == vlc.State.Playing: - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING elif status == vlc.State.Paused: - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED else: - self._state = MediaPlayerState.IDLE - self._media_duration = self._vlc.get_length() / 1000 - position = self._vlc.get_position() * self._media_duration - if position != self._media_position: - self._media_position_updated_at = dt_util.utcnow() - self._media_position = position + self._attr_state = MediaPlayerState.IDLE + self._attr_media_duration = self._vlc.get_length() / 1000 + position = self._vlc.get_position() * self._attr_media_duration + if position != self._attr_media_position: + self._attr_media_position_updated_at = dt_util.utcnow() + self._attr_media_position = position - self._volume = self._vlc.audio_get_volume() / 100 - self._muted = self._vlc.audio_get_mute() == 1 + self._attr_volume_level = self._vlc.audio_get_volume() / 100 + self._attr_is_volume_muted = self._vlc.audio_get_mute() == 1 return True - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - return self._media_duration - - @property - def media_position(self): - """Position of current playing media in seconds.""" - return self._media_position - - @property - def media_position_updated_at(self): - """When was the position of the current playing media valid.""" - return self._media_position_updated_at - def media_seek(self, position: float) -> None: """Seek the media to a specific location.""" track_length = self._vlc.get_length() / 1000 @@ -138,27 +97,27 @@ class VlcDevice(MediaPlayerEntity): def mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._vlc.audio_set_mute(mute) - self._muted = mute + self._attr_is_volume_muted = mute def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._vlc.audio_set_volume(int(volume * 100)) - self._volume = volume + self._attr_volume_level = volume def media_play(self) -> None: """Send play command.""" self._vlc.play() - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def media_pause(self) -> None: """Send pause command.""" self._vlc.pause() - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED def media_stop(self) -> None: """Send stop command.""" self._vlc.stop() - self._state = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any @@ -186,7 +145,7 @@ class VlcDevice(MediaPlayerEntity): self._vlc.play() await self.hass.async_add_executor_job(play) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING async def async_browse_media( self, diff --git a/homeassistant/components/vlc_telnet/translations/bg.json b/homeassistant/components/vlc_telnet/translations/bg.json index 41e4a0484f6..5d7713be732 100644 --- a/homeassistant/components/vlc_telnet/translations/bg.json +++ b/homeassistant/components/vlc_telnet/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/vlc_telnet/translations/sk.json b/homeassistant/components/vlc_telnet/translations/sk.json index d3bc93c4168..875dff682d3 100644 --- a/homeassistant/components/vlc_telnet/translations/sk.json +++ b/homeassistant/components/vlc_telnet/translations/sk.json @@ -1,16 +1,31 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, + "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Chcete sa pripoji\u0165 k doplnku {addon}?" + }, + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Zadajte spr\u00e1vne heslo pre hostite\u013ea: {host}" + }, "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov", + "password": "Heslo", "port": "Port" } } diff --git a/homeassistant/components/volumio/translations/sk.json b/homeassistant/components/volumio/translations/sk.json index 892b8b2cd91..26921034b40 100644 --- a/homeassistant/components/volumio/translations/sk.json +++ b/homeassistant/components/volumio/translations/sk.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Ned\u00e1 sa pripoji\u0165 k objavenej Volumio" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "port": "Port" } } diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 159cb39cf6a..0cd61a336b7 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -3,8 +3,7 @@ from __future__ import annotations from volvooncall.dashboard import Instrument -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/volvooncall/translations/bg.json b/homeassistant/components/volvooncall/translations/bg.json index 62a0a14568d..1227fb35873 100644 --- a/homeassistant/components/volvooncall/translations/bg.json +++ b/homeassistant/components/volvooncall/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", @@ -14,7 +14,6 @@ "mutable": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0438\u0441\u0442\u0430\u043d\u0446\u0438\u043e\u043d\u043d\u043e \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435 / \u0437\u0430\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u0438 \u0434\u0440.", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "region": "\u0420\u0435\u0433\u0438\u043e\u043d", - "scandinavian_miles": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u043a\u0430\u043d\u0434\u0438\u043d\u0430\u0432\u0441\u043a\u0438 \u043c\u0438\u043b\u0438", "unit_system": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0438", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } diff --git a/homeassistant/components/volvooncall/translations/ca.json b/homeassistant/components/volvooncall/translations/ca.json index b261c0dc095..4f5859b5f01 100644 --- a/homeassistant/components/volvooncall/translations/ca.json +++ b/homeassistant/components/volvooncall/translations/ca.json @@ -14,7 +14,6 @@ "mutable": "Permet l'engegada / bloqueig / etc, remot.", "password": "Contrasenya", "region": "Regi\u00f3", - "scandinavian_miles": "Utilitza milles escandinaves", "unit_system": "Sistema d'unitats", "username": "Nom d'usuari" } diff --git a/homeassistant/components/volvooncall/translations/de.json b/homeassistant/components/volvooncall/translations/de.json index 5d00fbb00a5..18da232c19e 100644 --- a/homeassistant/components/volvooncall/translations/de.json +++ b/homeassistant/components/volvooncall/translations/de.json @@ -14,7 +14,6 @@ "mutable": "Fernstart / -verriegelung / etc. zulassen", "password": "Passwort", "region": "Region", - "scandinavian_miles": "Skandinavische Meilen verwenden", "unit_system": "Einheitensystem", "username": "Benutzername" } diff --git a/homeassistant/components/volvooncall/translations/el.json b/homeassistant/components/volvooncall/translations/el.json index f216755a0b8..c845bec3eba 100644 --- a/homeassistant/components/volvooncall/translations/el.json +++ b/homeassistant/components/volvooncall/translations/el.json @@ -14,7 +14,6 @@ "mutable": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 / \u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 / \u03ba.\u03bb\u03c0.", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae", - "scandinavian_miles": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03a3\u03ba\u03b1\u03bd\u03b4\u03b9\u03bd\u03b1\u03b2\u03b9\u03ba\u03ce\u03bd \u039c\u03b9\u03bb\u03af\u03c9\u03bd", "unit_system": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03a3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } diff --git a/homeassistant/components/volvooncall/translations/en.json b/homeassistant/components/volvooncall/translations/en.json index fca96e5e0ed..55e5baa9b5c 100644 --- a/homeassistant/components/volvooncall/translations/en.json +++ b/homeassistant/components/volvooncall/translations/en.json @@ -14,7 +14,6 @@ "mutable": "Allow Remote Start / Lock / etc.", "password": "Password", "region": "Region", - "scandinavian_miles": "Use Scandinavian Miles", "unit_system": "Unit System", "username": "Username" } diff --git a/homeassistant/components/volvooncall/translations/es.json b/homeassistant/components/volvooncall/translations/es.json index bcca5a0da4d..548d634f29d 100644 --- a/homeassistant/components/volvooncall/translations/es.json +++ b/homeassistant/components/volvooncall/translations/es.json @@ -14,7 +14,6 @@ "mutable": "Permitir el arranque / bloqueo a distancia / etc.", "password": "Contrase\u00f1a", "region": "Regi\u00f3n", - "scandinavian_miles": "Utilizar millas escandinavas", "unit_system": "Sistema de unidades", "username": "Nombre de usuario" } diff --git a/homeassistant/components/volvooncall/translations/et.json b/homeassistant/components/volvooncall/translations/et.json index 9f2912b5d53..a14cbde6e5c 100644 --- a/homeassistant/components/volvooncall/translations/et.json +++ b/homeassistant/components/volvooncall/translations/et.json @@ -14,7 +14,6 @@ "mutable": "Luba kaugk\u00e4ivitus / lukustamine / jne.", "password": "Salas\u00f5na", "region": "Piirkond", - "scandinavian_miles": "Kasuta Scandinavian Miles", "unit_system": "\u00dchikute s\u00fcsteem", "username": "Kasutajanimi" } diff --git a/homeassistant/components/volvooncall/translations/fr.json b/homeassistant/components/volvooncall/translations/fr.json index 2449ab4bed4..c720557ccf2 100644 --- a/homeassistant/components/volvooncall/translations/fr.json +++ b/homeassistant/components/volvooncall/translations/fr.json @@ -14,7 +14,6 @@ "mutable": "Autoriser le d\u00e9marrage, le verrouillage, etc. \u00e0 distance", "password": "Mot de passe", "region": "R\u00e9gion", - "scandinavian_miles": "Utiliser les miles scandinaves", "unit_system": "Syst\u00e8me d'unit\u00e9s", "username": "Nom d'utilisateur" } diff --git a/homeassistant/components/volvooncall/translations/he.json b/homeassistant/components/volvooncall/translations/he.json index 6f2cdbf82e1..ad8caad16ae 100644 --- a/homeassistant/components/volvooncall/translations/he.json +++ b/homeassistant/components/volvooncall/translations/he.json @@ -1,9 +1,11 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/volvooncall/translations/hu.json b/homeassistant/components/volvooncall/translations/hu.json index 5b382780bea..e4d4b17c48f 100644 --- a/homeassistant/components/volvooncall/translations/hu.json +++ b/homeassistant/components/volvooncall/translations/hu.json @@ -14,7 +14,6 @@ "mutable": "Enged\u00e9lyezze a t\u00e1voli ind\u00edt\u00e1st / z\u00e1r\u00e1st / stb.", "password": "Jelsz\u00f3", "region": "R\u00e9gi\u00f3", - "scandinavian_miles": "Skandin\u00e1v m\u00e9rf\u00f6ld haszn\u00e1lata", "unit_system": "Egys\u00e9grendszer", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/volvooncall/translations/id.json b/homeassistant/components/volvooncall/translations/id.json index d4b60911401..a144ef366b5 100644 --- a/homeassistant/components/volvooncall/translations/id.json +++ b/homeassistant/components/volvooncall/translations/id.json @@ -14,7 +14,6 @@ "mutable": "Izinkan Mulai/Kunci Jarak Jauh, dll.", "password": "Kata Sandi", "region": "Wilayah", - "scandinavian_miles": "Gunakan Mil Skandinavia", "unit_system": "Sistem Unit", "username": "Nama Pengguna" } @@ -23,7 +22,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Proses konfigurasi platform Volvo On Call lewat YAML dalam proses penghapusan di versi mendatang Home Assistant.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "description": "Proses konfigurasi platform Volvo On Call lewat YAML dalam proses penghapusan di versi mendatang Home Assistant.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", "title": "Konfigurasi YAML Volvo On Call dalam proses penghapusan" } } diff --git a/homeassistant/components/volvooncall/translations/it.json b/homeassistant/components/volvooncall/translations/it.json index d96646b7873..781233e5356 100644 --- a/homeassistant/components/volvooncall/translations/it.json +++ b/homeassistant/components/volvooncall/translations/it.json @@ -14,7 +14,6 @@ "mutable": "Consenti da remoto l'avvio / il blocco / ecc.", "password": "Password", "region": "Regione", - "scandinavian_miles": "Usa le miglia scandinave", "unit_system": "Unit\u00e0 di misura", "username": "Nome utente" } diff --git a/homeassistant/components/volvooncall/translations/ja.json b/homeassistant/components/volvooncall/translations/ja.json index 4127b966710..3cd114643b0 100644 --- a/homeassistant/components/volvooncall/translations/ja.json +++ b/homeassistant/components/volvooncall/translations/ja.json @@ -14,7 +14,6 @@ "mutable": "\u30ea\u30e2\u30fc\u30c8\u30b9\u30bf\u30fc\u30c8/\u30ed\u30c3\u30af\u306a\u3069\u3092\u8a31\u53ef\u3057\u307e\u3059\u3002", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3", - "scandinavian_miles": "\u30b9\u30ab\u30f3\u30b8\u30ca\u30d3\u30a2\u30de\u30a4\u30eb(Scandinavian Miles)\u3092\u4f7f\u7528\u3059\u308b", "unit_system": "\u5358\u4f4d\u30b7\u30b9\u30c6\u30e0", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } diff --git a/homeassistant/components/volvooncall/translations/no.json b/homeassistant/components/volvooncall/translations/no.json index 48639f07b67..963ea6f1ca7 100644 --- a/homeassistant/components/volvooncall/translations/no.json +++ b/homeassistant/components/volvooncall/translations/no.json @@ -14,7 +14,6 @@ "mutable": "Tillat fjernstart / l\u00e5s / etc.", "password": "Passord", "region": "Region", - "scandinavian_miles": "Bruk Skandinaviske Miles", "unit_system": "Enhetssystem", "username": "Brukernavn" } diff --git a/homeassistant/components/volvooncall/translations/pl.json b/homeassistant/components/volvooncall/translations/pl.json index 5bdf7a253b5..fa69a7e1a51 100644 --- a/homeassistant/components/volvooncall/translations/pl.json +++ b/homeassistant/components/volvooncall/translations/pl.json @@ -14,7 +14,6 @@ "mutable": "Zezwalaj na zdalne uruchamianie / zamykanie / itp.", "password": "Has\u0142o", "region": "Region", - "scandinavian_miles": "U\u017cywaj skandynawskich mil", "unit_system": "System metryczny", "username": "Nazwa u\u017cytkownika" } diff --git a/homeassistant/components/volvooncall/translations/pt-BR.json b/homeassistant/components/volvooncall/translations/pt-BR.json index d8bb8a945d4..3a8ded27da9 100644 --- a/homeassistant/components/volvooncall/translations/pt-BR.json +++ b/homeassistant/components/volvooncall/translations/pt-BR.json @@ -14,7 +14,6 @@ "mutable": "Permitir partida remota / bloqueio / etc.", "password": "Senha", "region": "Regi\u00e3o", - "scandinavian_miles": "Usar milhas escandinavas", "unit_system": "Sistema de Unidades", "username": "Usu\u00e1rio" } diff --git a/homeassistant/components/volvooncall/translations/pt.json b/homeassistant/components/volvooncall/translations/pt.json index 232da9d130d..b89c4a9f74a 100644 --- a/homeassistant/components/volvooncall/translations/pt.json +++ b/homeassistant/components/volvooncall/translations/pt.json @@ -4,8 +4,7 @@ "user": { "data": { "mutable": "Permitir Partida/Bloqueio Remoto/etc.", - "region": "Regi\u00e3o", - "scandinavian_miles": "Use milhas escandinavas" + "region": "Regi\u00e3o" } } } diff --git a/homeassistant/components/volvooncall/translations/ru.json b/homeassistant/components/volvooncall/translations/ru.json index 3a125859e72..a1924c539cf 100644 --- a/homeassistant/components/volvooncall/translations/ru.json +++ b/homeassistant/components/volvooncall/translations/ru.json @@ -14,7 +14,6 @@ "mutable": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0434\u0438\u0441\u0442\u0430\u043d\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u0443\u0441\u043a / \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443 \u0438 \u0442.\u0434.", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c", - "scandinavian_miles": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043a\u0430\u043d\u0434\u0438\u043d\u0430\u0432\u0441\u043a\u0438\u0435 \u043c\u0438\u043b\u0438", "unit_system": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043c\u0435\u0440", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } diff --git a/homeassistant/components/volvooncall/translations/sk.json b/homeassistant/components/volvooncall/translations/sk.json new file mode 100644 index 00000000000..c249a185495 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "region": "Regi\u00f3n", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/sv.json b/homeassistant/components/volvooncall/translations/sv.json index 48d56656c6a..a8fc9c1c058 100644 --- a/homeassistant/components/volvooncall/translations/sv.json +++ b/homeassistant/components/volvooncall/translations/sv.json @@ -14,7 +14,6 @@ "mutable": "Till\u00e5t fj\u00e4rrstart / l\u00e5s / etc.", "password": "L\u00f6senord", "region": "Region", - "scandinavian_miles": "Anv\u00e4nd Skandinaviska mil", "unit_system": "Enhetssystem", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/volvooncall/translations/tr.json b/homeassistant/components/volvooncall/translations/tr.json index 4db94970086..3052feeb865 100644 --- a/homeassistant/components/volvooncall/translations/tr.json +++ b/homeassistant/components/volvooncall/translations/tr.json @@ -14,7 +14,6 @@ "mutable": "Uzaktan \u00c7al\u0131\u015ft\u0131rmaya / Kilitlemeye / vb. izin verin.", "password": "Parola", "region": "B\u00f6lge", - "scandinavian_miles": "\u0130skandinav Millerini Kullan\u0131n", "unit_system": "Birim Sistemi", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/volvooncall/translations/zh-Hant.json b/homeassistant/components/volvooncall/translations/zh-Hant.json index 65aeee8325f..e52f960a406 100644 --- a/homeassistant/components/volvooncall/translations/zh-Hant.json +++ b/homeassistant/components/volvooncall/translations/zh-Hant.json @@ -14,7 +14,6 @@ "mutable": "\u5141\u8a31\u9060\u7aef\u555f\u52d5/\u4e0a\u9396/\u7b49\u529f\u80fd\u3002", "password": "\u5bc6\u78bc", "region": "\u5340\u57df", - "scandinavian_miles": "\u4f7f\u7528\u7d0d\u7dad\u4e9e\u82f1\u91cc", "unit_system": "\u55ae\u4f4d\u7cfb\u7d71", "username": "\u4f7f\u7528\u8005\u540d\u7a31" } diff --git a/homeassistant/components/vulcan/translations/de.json b/homeassistant/components/vulcan/translations/de.json index 66de1c8ec75..b6736e3c096 100644 --- a/homeassistant/components/vulcan/translations/de.json +++ b/homeassistant/components/vulcan/translations/de.json @@ -4,7 +4,7 @@ "all_student_already_configured": "Alle Sch\u00fcler wurden bereits hinzugef\u00fcgt.", "already_configured": "Dieser Sch\u00fcler wurde bereits hinzugef\u00fcgt.", "no_matching_entries": "Keine \u00fcbereinstimmenden Eintr\u00e4ge gefunden, bitte verwende ein anderes Konto oder entferne die Integration mit veraltetem Sch\u00fcler.", - "reauth_successful": "Reauth erfolgreich" + "reauth_successful": "Reauthentifizierung erfolgreich" }, "error": { "cannot_connect": "Verbindungsfehler - Bitte \u00fcberpr\u00fcfe deine Internetverbindung", @@ -28,7 +28,7 @@ "region": "Symbol", "token": "Token" }, - "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." + "description": "Melde dich bei deinem Vulcan Konto \u00fcber die Registrierungsseite der mobilen App an." }, "reauth_confirm": { "data": { @@ -36,7 +36,7 @@ "region": "Symbol", "token": "Token" }, - "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." + "description": "Melde dich bei deinem Vulcan Konto \u00fcber die Registrierungsseite der mobilen App an." }, "select_saved_credentials": { "data": { @@ -48,7 +48,7 @@ "data": { "student_name": "Sch\u00fcler ausw\u00e4hlen" }, - "description": "W\u00e4hle Sch\u00fcler aus, Du kannst weitere Sch\u00fcler hinzuf\u00fcgen, indem du die Integration erneut hinzuf\u00fcgst." + "description": "W\u00e4hle Sch\u00fcler aus, du kannst weitere Sch\u00fcler hinzuf\u00fcgen, indem du die Integration erneut hinzuf\u00fcgst." } } } diff --git a/homeassistant/components/vulcan/translations/sk.json b/homeassistant/components/vulcan/translations/sk.json index c16ed208d24..0a366f7ef1b 100644 --- a/homeassistant/components/vulcan/translations/sk.json +++ b/homeassistant/components/vulcan/translations/sk.json @@ -1,8 +1,36 @@ { "config": { + "abort": { + "all_student_already_configured": "V\u0161etci \u0161tudenti u\u017e boli pridan\u00ed.", + "already_configured": "Tento \u0161tudent u\u017e bol pridan\u00fd." + }, + "error": { + "cannot_connect": "Chyba pripojenia \u2013 skontrolujte svoje internetov\u00e9 pripojenie", + "expired_token": "Platnos\u0165 tokenu vypr\u0161ala \u2013 vygenerujte si nov\u00fd token", + "invalid_pin": "Neplatn\u00fd k\u00f3d PIN", + "invalid_token": "Neplatn\u00fd token", + "unknown": "Vyskytla sa nezn\u00e1ma chyba" + }, "step": { "add_next_config_entry": { "description": "Prida\u0165 \u010fal\u0161ieho \u0161tudenta." + }, + "auth": { + "data": { + "pin": "Pin", + "token": "Token" + } + }, + "reauth_confirm": { + "data": { + "pin": "Pin", + "token": "Token" + } + }, + "select_saved_credentials": { + "data": { + "credentials": "Prihl\u00e1si\u0165" + } } } } diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index 4ef35d4f410..1d877216e93 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -5,7 +5,11 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, + BinarySensorDeviceClass, + BinarySensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -32,7 +36,6 @@ from . import ( _LOGGER = logging.getLogger(__name__) -DEFAULT_DEVICE_CLASS = "power" DEFAULT_NAME = "Vultr {}" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -64,7 +67,7 @@ def setup_platform( class VultrBinarySensor(BinarySensorEntity): """Representation of a Vultr subscription sensor.""" - _attr_device_class = DEFAULT_DEVICE_CLASS + _attr_device_class = BinarySensorDeviceClass.POWER def __init__(self, vultr, subscription, name): """Initialize a new Vultr binary sensor.""" diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 1b3904f63e6..446df402c87 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -8,7 +8,10 @@ from typing import Any import voluptuous as vol import wakeonlan -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, @@ -32,7 +35,7 @@ CONF_OFF_ACTION = "turn_off" DEFAULT_NAME = "Wake on LAN" DEFAULT_PING_TIMEOUT = 1 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, @@ -51,12 +54,12 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a wake on lan switch.""" - broadcast_address = config.get(CONF_BROADCAST_ADDRESS) - broadcast_port = config.get(CONF_BROADCAST_PORT) - host = config.get(CONF_HOST) - mac_address = config[CONF_MAC] - name = config[CONF_NAME] - off_action = config.get(CONF_OFF_ACTION) + broadcast_address: str | None = config.get(CONF_BROADCAST_ADDRESS) + broadcast_port: int | None = config.get(CONF_BROADCAST_PORT) + host: str | None = config.get(CONF_HOST) + mac_address: str = config[CONF_MAC] + name: str = config[CONF_NAME] + off_action: list[Any] | None = config.get(CONF_OFF_ACTION) add_entities( [ @@ -79,17 +82,16 @@ class WolSwitch(SwitchEntity): def __init__( self, - hass, - name, - host, - mac_address, - off_action, - broadcast_address, - broadcast_port, - ): + hass: HomeAssistant, + name: str, + host: str | None, + mac_address: str, + off_action: list[Any] | None, + broadcast_address: str | None, + broadcast_port: int | None, + ) -> None: """Initialize the WOL switch.""" - self._hass = hass - self._name = name + self._attr_name = name self._host = host self._mac_address = mac_address self._broadcast_address = broadcast_address @@ -98,37 +100,18 @@ class WolSwitch(SwitchEntity): Script(hass, off_action, name, DOMAIN) if off_action else None ) self._state = False - self._assumed_state = host is None - self._unique_id = dr.format_mac(mac_address) + self._attr_assumed_state = host is None + self._attr_should_poll = bool(not self._attr_assumed_state) + self._attr_unique_id = dr.format_mac(mac_address) @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" return self._state - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def assumed_state(self): - """Return true if no host is provided.""" - return self._assumed_state - - @property - def should_poll(self): - """Return false if assumed state is true.""" - return not self._assumed_state - - @property - def unique_id(self): - """Return the unique id of this switch.""" - return self._unique_id - def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - service_kwargs = {} + service_kwargs: dict[str, Any] = {} if self._broadcast_address is not None: service_kwargs["ip_address"] = self._broadcast_address if self._broadcast_port is not None: @@ -143,7 +126,7 @@ class WolSwitch(SwitchEntity): wakeonlan.send_magic_packet(self._mac_address, **service_kwargs) - if self._assumed_state: + if self._attr_assumed_state: self._state = True self.async_write_ha_state() @@ -152,7 +135,7 @@ class WolSwitch(SwitchEntity): if self._off_script is not None: self._off_script.run(context=self._context) - if self._assumed_state: + if self._attr_assumed_state: self._state = False self.async_write_ha_state() diff --git a/homeassistant/components/wake_on_lan/translations/bg.json b/homeassistant/components/wake_on_lan/translations/bg.json new file mode 100644 index 00000000000..daa173607c0 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/bg.json @@ -0,0 +1,7 @@ +{ + "issues": { + "moved_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Wake on Lan \u0435 \u043f\u0440\u0435\u043c\u0435\u0441\u0442\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/cs.json b/homeassistant/components/wake_on_lan/translations/cs.json new file mode 100644 index 00000000000..202a2726181 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/cs.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Konfigurace Wake on Lan pomoc\u00ed YAML byla p\u0159esunuta do integra\u010dn\u00edho kl\u00ed\u010de. \n\n Va\u0161e st\u00e1vaj\u00edc\u00ed konfigurace YAML bude fungovat pro dal\u0161\u00ed 2 verze. \n\n Prove\u010fte migraci konfigurace YAML na integra\u010dn\u00ed kl\u00ed\u010d podle dokumentace.", + "title": "Konfigurace Wake on Lan YAML byla p\u0159esunuta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/de.json b/homeassistant/components/wake_on_lan/translations/de.json new file mode 100644 index 00000000000..16105601211 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Die Konfiguration von Wake-on-LAN mit YAML wurde in den Integrationsschl\u00fcssel verschoben. \n\nDie vorhandene YAML-Konfiguration funktioniert f\u00fcr zwei weitere Versionen. \n\nMigriere deine YAML-Konfiguration gem\u00e4\u00df der Dokumentation zum Integrationsschl\u00fcssel.", + "title": "Die Wake-on-LAN YAML-Konfiguration wurde verschoben" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/el.json b/homeassistant/components/wake_on_lan/translations/el.json new file mode 100644 index 00000000000..a971c35c922 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Wake on Lan \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b5\u03c4\u03b1\u03c6\u03b5\u03c1\u03b8\u03b5\u03af \u03c3\u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2.\n\n\u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b8\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03b3\u03b9\u03b1 2 \u03b1\u03ba\u03cc\u03bc\u03b1 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2.\n\n\u039c\u03b5\u03c4\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c3\u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Wake on Lan YAML \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03b7\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/en.json b/homeassistant/components/wake_on_lan/translations/en.json new file mode 100644 index 00000000000..3d37fa4b9ba --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Configuring Wake on Lan using YAML has been moved to integration key.\n\nYour existing YAML configuration will be working for 2 more versions.\n\nMigrate your YAML configuration to the integration key according to the documentation.", + "title": "The Wake on Lan YAML configuration has been moved" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/es.json b/homeassistant/components/wake_on_lan/translations/es.json new file mode 100644 index 00000000000..e8500317ade --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "La configuraci\u00f3n de Wake on Lan usando YAML se ha movido a clave de integraci\u00f3n. \n\nTu configuraci\u00f3n YAML existente funcionar\u00e1 durante 2 versiones m\u00e1s. \n\nMigra tu configuraci\u00f3n YAML a clave de integraci\u00f3n de acuerdo con la documentaci\u00f3n.", + "title": "La configuraci\u00f3n YAML de Wake on Lan se ha movido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/et.json b/homeassistant/components/wake_on_lan/translations/et.json new file mode 100644 index 00000000000..e9f60abfede --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Wake on Lan'i seadistamine YAML-i abil on viidud integratsiooniv\u00f5tmesse.\n\nOlemasolev YAML-konfiguratsioon t\u00f6\u00f6tab veel 2 versiooni.\n\nMigreeri oma YAML-konfiguratsioon integratsiooniv\u00f5tmesse vastavalt dokumentatsioonile.", + "title": "Wake on Lan YAML-i konfiguratsioon on teisaldatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/hu.json b/homeassistant/components/wake_on_lan/translations/hu.json new file mode 100644 index 00000000000..83dcde2cdf5 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "A Wake-on-LAN yaml haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa megsz\u0171nt.\n\nA megl\u00e9v\u0151 YAML-konfigur\u00e1ci\u00f3 a k\u00f6vetkez\u0151 k\u00e9t verzi\u00f3ban fog m\u0171k\u00f6dni.\n\nHelyezze \u00e1t a YAML-konfigur\u00e1ci\u00f3t az integr\u00e1ci\u00f3ba a dokument\u00e1ci\u00f3nak megfelel\u0151en.", + "title": "A Wake on Lan YAML konfigur\u00e1ci\u00f3 meg fog sz\u0171nni" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/id.json b/homeassistant/components/wake_on_lan/translations/id.json new file mode 100644 index 00000000000..96b2cc2a2b2 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Konfigurasi Integrasi Wake-on-LAN menggunakan YAML telah dipindahkan ke kunci integrasi.\n\nKonfigurasi YAML Anda yang ada saat ini akan berfungsi hingga 2 versi berikutnya.\n\nMigrasikan konfigurasi YAML Anda ke kunci integrasi sesuai dengan dokumentasi.", + "title": "Konfigurasi YAML Integrasi Wake-on-LAN telah dipindahkan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/it.json b/homeassistant/components/wake_on_lan/translations/it.json new file mode 100644 index 00000000000..758ef17ae09 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "La configurazione di Wake on Lan tramite YAML \u00e8 stata spostata nella chiave di integrazione. \n\nLa tua configurazione YAML esistente funzioner\u00e0 per altre 2 versioni. \n\nMigra la tua configurazione YAML alla chiave di integrazione seguendo la documentazione.", + "title": "La configurazione YAML di Wake on Lan \u00e8 stata spostata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/nl.json b/homeassistant/components/wake_on_lan/translations/nl.json new file mode 100644 index 00000000000..61c7e21fd8b --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/nl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "De configuratie van Wake on LAN met YAML is verplaatst naar de integratiesleutel.\n\nDe bestaande YAML configuratie zal nog voor 2 versies werken.\n\nMigreer je YAML configuratie naar de integratiesleutel zoals is aangeven in de documentatie.", + "title": "De Wake on Lan YAML configuratie is verplaatst" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/no.json b/homeassistant/components/wake_on_lan/translations/no.json new file mode 100644 index 00000000000..37148c404d8 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Konfigurering av Wake on Lan ved hjelp av YAML har blitt flyttet til integrasjonsn\u00f8kkel. \n\n Din eksisterende YAML-konfigurasjon vil fungere for 2 flere versjoner. \n\n Migrer YAML-konfigurasjonen til integrasjonsn\u00f8kkelen i henhold til dokumentasjonen.", + "title": "Wake on Lan YAML-konfigurasjonen er flyttet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/pl.json b/homeassistant/components/wake_on_lan/translations/pl.json new file mode 100644 index 00000000000..3d230c99282 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Konfiguracja Wake on LAN za pomoc\u0105 YAML zosta\u0142a przeniesiona do klucza integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML b\u0119dzie dzia\u0142a\u0107 przez kolejne 2 wersje. \n\nPrzenie\u015b swoj\u0105 konfiguracj\u0119 YAML do klucza integracji zgodnie z dokumentacj\u0105.", + "title": "Konfiguracja YAML dla Wake on LAN zostaje przeniesiona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/pt-BR.json b/homeassistant/components/wake_on_lan/translations/pt-BR.json new file mode 100644 index 00000000000..b685bc04ad5 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "A configura\u00e7\u00e3o do Wake on Lan usando YAML foi movida para a chave de integra\u00e7\u00e3o. \n\n Sua configura\u00e7\u00e3o YAML existente funcionar\u00e1 para mais 2 vers\u00f5es. \n\n Migre sua configura\u00e7\u00e3o YAML para a chave de integra\u00e7\u00e3o de acordo com a documenta\u00e7\u00e3o.", + "title": "A configura\u00e7\u00e3o YAML de Wake on Lan foi movida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/ru.json b/homeassistant/components/wake_on_lan/translations/ru.json new file mode 100644 index 00000000000..cc9445ad551 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/ru.json @@ -0,0 +1,7 @@ +{ + "issues": { + "moved_yaml": { + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML Wake on Lan \u0431\u044b\u043b\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/zh-Hant.json b/homeassistant/components/wake_on_lan/translations/zh-Hant.json new file mode 100644 index 00000000000..f62b98371a2 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Wake on Lan \u5373\u5c07\u8f49\u79fb\u81f3\u6574\u5408\u3002\n\n\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u53ea\u80fd\u518d\u4f7f\u7528\u5169\u500b\u66f4\u65b0\u7248\u672c\u3002\n\n\u8ddf\u96a8\u6587\u4ef6\u8aaa\u660e\u9077\u79fb YAML \u8a2d\u5b9a\u81f3\u6574\u5408\u3002", + "title": "Wake on Lan YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 6382cf05940..b5e935c27f1 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -21,8 +21,10 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + CHARGER_CURRENCY_KEY, CHARGER_CURRENT_VERSION_KEY, CHARGER_DATA_KEY, + CHARGER_ENERGY_PRICE_KEY, CHARGER_LOCKED_UNLOCKED_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, CHARGER_NAME_KEY, @@ -31,6 +33,7 @@ from .const import ( CHARGER_SOFTWARE_KEY, CHARGER_STATUS_DESCRIPTION_KEY, CHARGER_STATUS_ID_KEY, + CODE_KEY, CONF_STATION, DOMAIN, ChargerStatus, @@ -124,6 +127,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][ CHARGER_LOCKED_UNLOCKED_KEY ] + data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][ + CHARGER_ENERGY_PRICE_KEY + ] + data[ + CHARGER_CURRENCY_KEY + ] = f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" + data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN ) @@ -194,7 +204,11 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Wallbox from a config entry.""" - wallbox = Wallbox(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) + wallbox = Wallbox( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + jwtTokenDrift=UPDATE_INTERVAL, + ) wallbox_coordinator = WallboxCoordinator( entry.data[CONF_STATION], wallbox, diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 9d1db637879..a6f92541a10 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -5,6 +5,7 @@ DOMAIN = "wallbox" BIDIRECTIONAL_MODEL_PREFIXES = ["QSX"] +CODE_KEY = "code" CONF_STATION = "station" CHARGER_ADDED_DISCHARGED_ENERGY_KEY = "added_discharged_energy" CHARGER_ADDED_ENERGY_KEY = "added_energy" @@ -15,8 +16,10 @@ CHARGER_CHARGING_TIME_KEY = "charging_time" CHARGER_COST_KEY = "cost" CHARGER_CURRENT_MODE_KEY = "current_mode" CHARGER_CURRENT_VERSION_KEY = "currentVersion" +CHARGER_CURRENCY_KEY = "currency" CHARGER_DATA_KEY = "config_data" CHARGER_DEPOT_PRICE_KEY = "depot_price" +CHARGER_ENERGY_PRICE_KEY = "energy_price" CHARGER_SERIAL_NUMBER_KEY = "serial_number" CHARGER_PART_NUMBER_KEY = "part_number" CHARGER_SOFTWARE_KEY = "software" diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 433a759bea5..e1d64ab9478 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -3,7 +3,7 @@ "name": "Wallbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", - "requirements": ["wallbox==0.4.10"], + "requirements": ["wallbox==0.4.12"], "codeowners": ["@hesselonline"], "iot_class": "cloud_polling", "loggers": ["wallbox"] diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 1fae68da2e4..9ce76d59608 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -31,9 +31,11 @@ from .const import ( CHARGER_CHARGING_POWER_KEY, CHARGER_CHARGING_SPEED_KEY, CHARGER_COST_KEY, + CHARGER_CURRENCY_KEY, CHARGER_CURRENT_MODE_KEY, CHARGER_DATA_KEY, CHARGER_DEPOT_PRICE_KEY, + CHARGER_ENERGY_PRICE_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, CHARGER_SERIAL_NUMBER_KEY, @@ -127,6 +129,14 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { icon="mdi:ev-station", name="Depot Price", precision=2, + state_class=SensorStateClass.MEASUREMENT, + ), + CHARGER_ENERGY_PRICE_KEY: WallboxSensorEntityDescription( + key=CHARGER_ENERGY_PRICE_KEY, + icon="mdi:ev-station", + name="Energy Price", + precision=2, + state_class=SensorStateClass.MEASUREMENT, ), CHARGER_STATUS_DESCRIPTION_KEY: WallboxSensorEntityDescription( key=CHARGER_STATUS_DESCRIPTION_KEY, @@ -188,3 +198,13 @@ class WallboxSensor(WallboxEntity, SensorEntity): round(self.coordinator.data[self.entity_description.key], sensor_round), ) return cast(StateType, self.coordinator.data[self.entity_description.key]) + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor. When monetary, get the value from the api.""" + if self.entity_description.key in ( + CHARGER_ENERGY_PRICE_KEY, + CHARGER_DEPOT_PRICE_KEY, + ): + return cast(str, self.coordinator.data[CHARGER_CURRENCY_KEY]) + return cast(str, self.entity_description.native_unit_of_measurement) diff --git a/homeassistant/components/wallbox/translations/bg.json b/homeassistant/components/wallbox/translations/bg.json index cc0ea3ece2d..b221ff4ddb8 100644 --- a/homeassistant/components/wallbox/translations/bg.json +++ b/homeassistant/components/wallbox/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/wallbox/translations/sk.json b/homeassistant/components/wallbox/translations/sk.json index 71a7aea5018..f0bfe254739 100644 --- a/homeassistant/components/wallbox/translations/sk.json +++ b/homeassistant/components/wallbox/translations/sk.json @@ -1,10 +1,29 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "reauth_invalid": "Op\u00e4tovn\u00e9 overenie zlyhalo; s\u00e9riov\u00e9 \u010d\u00edslo sa nezhoduje s p\u00f4vodn\u00fdm", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "password": "Heslo", + "station": "S\u00e9riov\u00e9 \u010d\u00edslo stanice", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 4712d4a2701..c821824d279 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta -from enum import IntEnum +from enum import IntFlag import functools as ft import logging from typing import Any, final @@ -56,7 +56,7 @@ STATE_HEAT_PUMP = "heat_pump" STATE_GAS = "gas" -class WaterHeaterEntityFeature(IntEnum): +class WaterHeaterEntityFeature(IntFlag): """Supported features of the fan entity.""" TARGET_TEMPERATURE = 1 @@ -167,7 +167,7 @@ class WaterHeaterEntity(Entity): _attr_operation_list: list[str] | None = None _attr_precision: float _attr_state: None = None - _attr_supported_features: int + _attr_supported_features: WaterHeaterEntityFeature = WaterHeaterEntityFeature(0) _attr_target_temperature_high: float | None = None _attr_target_temperature_low: float | None = None _attr_target_temperature: float | None = None @@ -191,8 +191,6 @@ class WaterHeaterEntity(Entity): @property def capability_attributes(self) -> Mapping[str, Any]: """Return capability attributes.""" - supported_features = self.supported_features or 0 - data: dict[str, Any] = { ATTR_MIN_TEMP: show_temp( self.hass, self.min_temp, self.temperature_unit, self.precision @@ -202,7 +200,7 @@ class WaterHeaterEntity(Entity): ), } - if supported_features & WaterHeaterEntityFeature.OPERATION_MODE: + if self.supported_features & WaterHeaterEntityFeature.OPERATION_MODE: data[ATTR_OPERATION_LIST] = self.operation_list return data @@ -238,12 +236,10 @@ class WaterHeaterEntity(Entity): ), } - supported_features = self.supported_features or 0 - - if supported_features & WaterHeaterEntityFeature.OPERATION_MODE: + if self.supported_features & WaterHeaterEntityFeature.OPERATION_MODE: data[ATTR_OPERATION_MODE] = self.current_operation - if supported_features & WaterHeaterEntityFeature.AWAY_MODE: + if self.supported_features & WaterHeaterEntityFeature.AWAY_MODE: is_away = self.is_away_mode_on data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF @@ -341,6 +337,11 @@ class WaterHeaterEntity(Entity): DEFAULT_MAX_TEMP, TEMP_FAHRENHEIT, self.temperature_unit ) + @property + def supported_features(self) -> WaterHeaterEntityFeature: + """Return the list of supported features.""" + return self._attr_supported_features + async def async_service_away_mode( entity: WaterHeaterEntity, service: ServiceCall diff --git a/homeassistant/components/water_heater/translations/sk.json b/homeassistant/components/water_heater/translations/sk.json new file mode 100644 index 00000000000..61841c12639 --- /dev/null +++ b/homeassistant/components/water_heater/translations/sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "off": "Neakt\u00edvny" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 93e6b98fbd6..efd20e37e83 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -23,6 +23,7 @@ CONF_TEXT_TYPE = "text" SUPPORTED_VOICES = [ "ar-AR_OmarVoice", "ar-MS_OmarVoice", + "cs-CZ_AlenaVoice", "de-DE_BirgitV2Voice", "de-DE_BirgitV3Voice", "de-DE_BirgitVoice", @@ -32,21 +33,26 @@ SUPPORTED_VOICES = [ "de-DE_ErikaV3Voice", "en-AU_CraigVoice", "en-AU_MadisonVoice", + "en-AU_SteveVoice", "en-GB_KateV3Voice", "en-GB_KateVoice", "en-GB_CharlotteV3Voice", "en-GB_JamesV3Voice", "en-GB_KateV3Voice", "en-GB_KateVoice", + "en-US_AllisonExpressive", "en-US_AllisonV2Voice", "en-US_AllisonV3Voice", "en-US_AllisonVoice", "en-US_EmilyV3Voice", + "en-US_EmmaExpressive", "en-US_HenryV3Voice", "en-US_KevinV3Voice", + "en-US_LisaExpressive", "en-US_LisaV2Voice", "en-US_LisaV3Voice", "en-US_LisaVoice", + "en-US_MichaelExpressive", "en-US_MichaelV2Voice", "en-US_MichaelV3Voice", "en-US_MichaelVoice", @@ -72,10 +78,13 @@ SUPPORTED_VOICES = [ "ko-KR_SiWooVoice", "ko-KR_YoungmiVoice", "ko-KR_YunaVoice", + "nl-BE_AdeleVoice", + "nl-BE_BramVoice", "nl-NL_EmmaVoice", "nl-NL_LiamVoice", "pt-BR_IsabelaV3Voice", "pt-BR_IsabelaVoice", + "sv-SE_IngridVoice", "zh-CN_LiNaVoice", "zh-CN_WangWeiVoice", "zh-CN_ZhangJingVoice", @@ -83,8 +92,13 @@ SUPPORTED_VOICES = [ DEPRECATED_VOICES = [ "ar-AR_OmarVoice", + "ar-MS_OmarVoice", + "cs-CZ_AlenaVoice", "de-DE_BirgitVoice", "de-DE_DieterVoice", + "en-AU_CraigVoice", + "en-AU_MadisonVoice", + "en-AU_SteveVoice", "en-GB_KateVoice", "en-GB_KateV3Voice", "en-US_AllisonVoice", @@ -97,7 +111,19 @@ DEPRECATED_VOICES = [ "fr-FR_ReneeVoice", "it-IT_FrancescaVoice", "ja-JP_EmiVoice", + "ko-KR_HyunjunVoice", + "ko-KR_SiWooVoice", + "ko-KR_YoungmiVoice", + "ko-KR_YunaVoice", + "nl-BE_AdeleVoice", + "nl-BE_BramVoice", + "nl-NL_EmmaVoice", + "nl-NL_LiamVoice", "pt-BR_IsabelaVoice", + "sv-SE_IngridVoice", + "zh-CN_LiNaVoice", + "zh-CN_WangWeiVoice", + "zh-CN_ZhangJingVoice", ] SUPPORTED_OUTPUT_FORMATS = [ diff --git a/homeassistant/components/watttime/translations/bg.json b/homeassistant/components/watttime/translations/bg.json index 0ce2b541513..62b9a11a068 100644 --- a/homeassistant/components/watttime/translations/bg.json +++ b/homeassistant/components/watttime/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/watttime/translations/sk.json b/homeassistant/components/watttime/translations/sk.json index 1d9ecbee3fc..a18c17ea0e1 100644 --- a/homeassistant/components/watttime/translations/sk.json +++ b/homeassistant/components/watttime/translations/sk.json @@ -1,21 +1,49 @@ { "config": { "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", + "unknown_coordinates": "\u017diadne \u00fadaje o zemepisnej \u0161\u00edrke/d\u013a\u017eke" }, "step": { "coordinates": { "data": { "latitude": "Zemepisn\u00e1 \u0161\u00edrka", "longitude": "Zemepisn\u00e1 d\u013a\u017eka" - } + }, + "description": "Zadajte zemepisn\u00fa \u0161\u00edrku a d\u013a\u017eku, ktor\u00e9 chcete sledova\u0165:" }, "location": { "data": { "location_type": "Umiestnenie" + }, + "description": "Vyberte miesto, ktor\u00e9 chcete monitorova\u0165:" + }, + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Znova zadajte heslo pre {username}:", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte pou\u017e\u00edvate\u013esk\u00e9 meno a heslo:" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Zobrazi\u0165 monitorovan\u00fa polohu na mape" } } } diff --git a/homeassistant/components/waze_travel_time/translations/bg.json b/homeassistant/components/waze_travel_time/translations/bg.json index 5b18b5ba021..295b56e5daa 100644 --- a/homeassistant/components/waze_travel_time/translations/bg.json +++ b/homeassistant/components/waze_travel_time/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json index 42ef4698151..a12552c4870 100644 --- a/homeassistant/components/waze_travel_time/translations/de.json +++ b/homeassistant/components/waze_travel_time/translations/de.json @@ -31,7 +31,7 @@ "units": "Einheiten", "vehicle_type": "Fahrzeugtyp" }, - "description": "Mit den \"Substring\"-Eintr\u00e4gen kannst du die Integration zwingen, eine bestimmte Route zu verwenden oder eine bestimmte Route bei der Zeitreiseberechnung zu vermeiden." + "description": "Mit den 'Substring'-Eintr\u00e4gen kannst du die Integration zwingen, eine bestimmte Route zu verwenden oder eine bestimmte Route bei der Zeitreiseberechnung zu vermeiden." } } }, diff --git a/homeassistant/components/waze_travel_time/translations/sk.json b/homeassistant/components/waze_travel_time/translations/sk.json index ce32d575ee2..6eef223b1a1 100644 --- a/homeassistant/components/waze_travel_time/translations/sk.json +++ b/homeassistant/components/waze_travel_time/translations/sk.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { @@ -10,5 +13,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "units": "Jednotky" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 8ffced6d5d2..610b8f7c355 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -11,24 +11,14 @@ from typing import Any, Final, TypedDict, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_MILLIMETERS, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_MBAR, - PRESSURE_MMHG, - SPEED_FEET_PER_SECOND, - SPEED_KILOMETERS_PER_HOUR, - SPEED_KNOTS, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -44,7 +34,7 @@ from homeassistant.util.unit_conversion import ( SpeedConverter, TemperatureConverter, ) -from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM _LOGGER = logging.getLogger(__name__) @@ -101,29 +91,29 @@ SCAN_INTERVAL = timedelta(seconds=30) ROUNDING_PRECISION = 2 VALID_UNITS_PRESSURE: set[str] = { - PRESSURE_HPA, - PRESSURE_MBAR, - PRESSURE_INHG, - PRESSURE_MMHG, + UnitOfPressure.HPA, + UnitOfPressure.MBAR, + UnitOfPressure.INHG, + UnitOfPressure.MMHG, } VALID_UNITS_TEMPERATURE: set[str] = { - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, } VALID_UNITS_PRECIPITATION: set[str] = { - LENGTH_MILLIMETERS, - LENGTH_INCHES, + UnitOfPrecipitationDepth.MILLIMETERS, + UnitOfPrecipitationDepth.INCHES, } VALID_UNITS_VISIBILITY: set[str] = { - LENGTH_KILOMETERS, - LENGTH_MILES, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, } VALID_UNITS_WIND_SPEED: set[str] = { - SPEED_FEET_PER_SECOND, - SPEED_KILOMETERS_PER_HOUR, - SPEED_KNOTS, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, + UnitOfSpeed.FEET_PER_SECOND, + UnitOfSpeed.KILOMETERS_PER_HOUR, + UnitOfSpeed.KNOTS, + UnitOfSpeed.METERS_PER_SECOND, + UnitOfSpeed.MILES_PER_HOUR, } UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { @@ -420,9 +410,9 @@ class WeatherEntity(Entity): Should not be set by integrations. """ - return ( - PRESSURE_HPA if self.hass.config.units is METRIC_SYSTEM else PRESSURE_INHG - ) + if self.hass.config.units is US_CUSTOMARY_SYSTEM: + return UnitOfPressure.INHG + return UnitOfPressure.HPA @final @property @@ -484,11 +474,9 @@ class WeatherEntity(Entity): Should not be set by integrations. """ - return ( - SPEED_KILOMETERS_PER_HOUR - if self.hass.config.units is METRIC_SYSTEM - else SPEED_MILES_PER_HOUR - ) + if self.hass.config.units is US_CUSTOMARY_SYSTEM: + return UnitOfSpeed.MILES_PER_HOUR + return UnitOfSpeed.KILOMETERS_PER_HOUR @final @property @@ -623,7 +611,7 @@ class WeatherEntity(Entity): return self._attr_precision return ( PRECISION_TENTHS - if self._temperature_unit == TEMP_CELSIUS + if self._temperature_unit == UnitOfTemperature.CELSIUS else PRECISION_WHOLE ) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 8b023990590..cd5485d4fd2 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1,10 +1,8 @@ """Support for LG webOS Smart TV.""" from __future__ import annotations -from collections.abc import Callable, Coroutine from contextlib import suppress import logging -from typing import Any from aiowebostv import WebOsClient, WebOsTvPairError import voluptuous as vol @@ -19,17 +17,9 @@ from homeassistant.const import ( CONF_NAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import ( - Context, - Event, - HassJob, - HomeAssistant, - ServiceCall, - callback, -) +from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.trigger import TriggerActionType from homeassistant.helpers.typing import ConfigType from .const import ( @@ -165,43 +155,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class PluggableAction: - """A pluggable action handler.""" - - def __init__(self) -> None: - """Initialize.""" - self._actions: dict[ - Callable[[], None], - tuple[HassJob[..., Coroutine[Any, Any, None]], dict[str, Any]], - ] = {} - - def __bool__(self) -> bool: - """Return if we have something attached.""" - return bool(self._actions) - - @callback - def async_attach( - self, action: TriggerActionType, variables: dict[str, Any] - ) -> Callable[[], None]: - """Attach a device trigger for turn on.""" - - @callback - def _remove() -> None: - del self._actions[_remove] - - job = HassJob(action) - - self._actions[_remove] = (job, variables) - - return _remove - - @callback - def async_run(self, hass: HomeAssistant, context: Context | None = None) -> None: - """Run all turn on triggers.""" - for job, variables in self._actions.values(): - hass.async_run_hass_job(job, variables, context) - - class WebOsClientWrapper: """Wrapper for a WebOS TV client with Home Assistant specific functions.""" @@ -209,7 +162,6 @@ class WebOsClientWrapper: """Set up the client.""" self.host = host self.client_key = client_key - self.turn_on = PluggableAction() self.client: WebOsClient | None = None async def connect(self) -> None: diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index ef3e74a7daa..14854383ec8 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -7,7 +7,7 @@ from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEM from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import CONF_DEVICE_ID, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo @@ -18,9 +18,11 @@ from .const import DOMAIN from .helpers import ( async_get_client_wrapper_by_device_entry, async_get_device_entry_by_device_id, - async_is_device_config_entry_not_loaded, ) -from .triggers.turn_on import PLATFORM_TYPE as TURN_ON_PLATFORM_TYPE +from .triggers.turn_on import ( + PLATFORM_TYPE as TURN_ON_PLATFORM_TYPE, + async_get_turn_on_trigger, +) TRIGGER_TYPES = {TURN_ON_PLATFORM_TYPE} TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -36,12 +38,6 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) - try: - if async_is_device_config_entry_not_loaded(hass, config[CONF_DEVICE_ID]): - return config - except ValueError as err: - raise InvalidDeviceAutomationConfig(err) from err - if config[CONF_TYPE] == TURN_ON_PLATFORM_TYPE: device_id = config[CONF_DEVICE_ID] try: @@ -58,15 +54,7 @@ async def async_get_triggers( _hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for device.""" - triggers = [] - base_trigger = { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - } - - triggers.append({**base_trigger, CONF_TYPE: TURN_ON_PLATFORM_TYPE}) - + triggers = [async_get_turn_on_trigger(device_id)] return triggers diff --git a/homeassistant/components/webostv/helpers.py b/homeassistant/components/webostv/helpers.py index 0ee3805f42f..4f1ab9dfebe 100644 --- a/homeassistant/components/webostv/helpers.py +++ b/homeassistant/components/webostv/helpers.py @@ -1,7 +1,6 @@ """Helper functions for webOS Smart TV.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry @@ -26,19 +25,6 @@ def async_get_device_entry_by_device_id( return device -@callback -def async_is_device_config_entry_not_loaded( - hass: HomeAssistant, device_id: str -) -> bool: - """Return whether device's config entries are not loaded.""" - device = async_get_device_entry_by_device_id(hass, device_id) - return any( - (entry := hass.config_entries.async_get_entry(entry_id)) - and entry.state != ConfigEntryState.LOADED - for entry_id in device.config_entries - ) - - @callback def async_get_device_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> str: """ diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 10fed607ee8..dcbec24c665 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -32,6 +32,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.trigger import PluggableAction from . import WebOsClientWrapper from .const import ( @@ -43,6 +44,7 @@ from .const import ( LIVE_TV_APP_ID, WEBOSTV_EXCEPTIONS, ) +from .triggers.turn_on import async_get_turn_on_trigger _LOGGER = logging.getLogger(__name__) @@ -133,17 +135,24 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): # Assume that the TV is not paused self._paused = False - + self._turn_on = PluggableAction(self.async_write_ha_state) self._current_source = None self._source_list: dict = {} - self._supported_features: int = 0 + self._supported_features = MediaPlayerEntityFeature(0) self._update_states() async def async_added_to_hass(self) -> None: """Connect and subscribe to dispatcher signals and state updates.""" await super().async_added_to_hass() + if (entry := self.registry_entry) and entry.device_id: + self.async_on_remove( + self._turn_on.async_register( + self.hass, async_get_turn_on_trigger(entry.device_id) + ) + ) + self.async_on_remove( async_dispatcher_connect(self.hass, DOMAIN, self.async_signal_handler) ) @@ -157,7 +166,9 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): and (state := await self.async_get_last_state()) is not None ): self._supported_features = ( - state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + state.attributes.get( + ATTR_SUPPORTED_FEATURES, MediaPlayerEntityFeature(0) + ) & ~MediaPlayerEntityFeature.TURN_ON ) @@ -314,9 +325,9 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): await self._client.connect() @property - def supported_features(self) -> int: + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - if self._wrapper.turn_on: + if self._turn_on: return self._supported_features | MediaPlayerEntityFeature.TURN_ON return self._supported_features @@ -328,7 +339,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): async def async_turn_on(self) -> None: """Turn on media player.""" - self._wrapper.turn_on.async_run(self.hass, self._context) + await self._turn_on.async_run(self.hass, self._context) @cmd async def async_volume_up(self) -> None: diff --git a/homeassistant/components/webostv/translations/de.json b/homeassistant/components/webostv/translations/de.json index f88412a1d67..22ac8b87663 100644 --- a/homeassistant/components/webostv/translations/de.json +++ b/homeassistant/components/webostv/translations/de.json @@ -19,7 +19,7 @@ "host": "Host", "name": "Name" }, - "description": "Schalte den TV ein, f\u00fclle die folgenden Felder aus und dr\u00fccke auf Senden", + "description": "Schalte den Fernseher ein, f\u00fclle die folgenden Felder aus und dr\u00fccke auf Senden", "title": "Mit webOS TV verbinden" } } diff --git a/homeassistant/components/webostv/translations/sk.json b/homeassistant/components/webostv/translations/sk.json index 57685686065..eb20925e0a4 100644 --- a/homeassistant/components/webostv/translations/sk.json +++ b/homeassistant/components/webostv/translations/sk.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov" }, "description": "Zapnite TV, vypl\u0148te nasleduj\u00face polia, kliknite na Odosla\u0165", diff --git a/homeassistant/components/webostv/triggers/turn_on.py b/homeassistant/components/webostv/triggers/turn_on.py index 806b0b4b964..403219f1372 100644 --- a/homeassistant/components/webostv/triggers/turn_on.py +++ b/homeassistant/components/webostv/triggers/turn_on.py @@ -3,15 +3,25 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.trigger import ( + PluggableAction, + TriggerActionType, + TriggerInfo, +) from homeassistant.helpers.typing import ConfigType from ..const import DOMAIN from ..helpers import ( - async_get_client_wrapper_by_device_entry, async_get_device_entry_by_device_id, async_get_device_id_from_entity_id, ) @@ -33,6 +43,17 @@ TRIGGER_SCHEMA = vol.All( ) +def async_get_turn_on_trigger(device_id: str) -> dict[str, str]: + """Return data for a turn on trigger.""" + + return { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: PLATFORM_TYPE, + } + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -69,10 +90,12 @@ async def async_attach_trigger( "description": f"webostv turn on trigger for {device_name}", } - client_wrapper = async_get_client_wrapper_by_device_entry(hass, device) + turn_on_trigger = async_get_turn_on_trigger(device_id) unsubs.append( - client_wrapper.turn_on.async_attach(action, {"trigger": variables}) + PluggableAction.async_attach_trigger( + hass, turn_on_trigger, action, {"trigger": variables} + ) ) @callback diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 19ec505e449..b4a18ab9ff0 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -446,7 +446,7 @@ async def handle_render_template( ) -> None: """Handle render_template command.""" template_str = msg["template"] - template_obj = template.Template(template_str, hass) # type: ignore[no-untyped-call] + template_obj = template.Template(template_str, hass) variables = msg.get("variables") timeout = msg.get("timeout") info = None diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 23c8fddd56c..acb5bf48131 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -214,7 +214,7 @@ class WebSocketHandler: disconnect_warn = "Did not receive auth message within 10 seconds" raise Disconnect from err - if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): raise Disconnect if msg.type != WSMsgType.TEXT: @@ -238,7 +238,7 @@ class WebSocketHandler: while not wsock.closed: msg = await wsock.receive() - if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): break if msg.type != WSMsgType.TEXT: diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index f40d2940561..73b05594bd5 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "integration_type": "system" + "integration_type": "system", + "after_dependencies": ["recorder"] } diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 6100c2ea13c..f3a0cebe51f 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -11,6 +11,10 @@ from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, ) +from homeassistant.components.recorder import ( + EVENT_RECORDER_5MIN_STATISTICS_GENERATED, + EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, +) from homeassistant.components.shopping_list import EVENT_SHOPPING_LIST_UPDATED from homeassistant.const import ( EVENT_COMPONENT_LOADED, @@ -35,6 +39,8 @@ SUBSCRIBE_ALLOWLIST: Final[set[str]] = { EVENT_LOVELACE_UPDATED, EVENT_PANELS_UPDATED, EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, + EVENT_RECORDER_5MIN_STATISTICS_GENERATED, + EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_SHOPPING_LIST_UPDATED, diff --git a/homeassistant/components/wemo/translations/he.json b/homeassistant/components/wemo/translations/he.json index 380dbc5d7fc..032c9c9fa17 100644 --- a/homeassistant/components/wemo/translations/he.json +++ b/homeassistant/components/wemo/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/wemo/translations/sk.json b/homeassistant/components/wemo/translations/sk.json new file mode 100644 index 00000000000..99798036ffd --- /dev/null +++ b/homeassistant/components/wemo/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index c6721197abc..0d6ae706f0c 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -10,7 +10,7 @@ from whirlpool.backendselector import BackendSelector, Brand, Region from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not auth.is_access_token_valid(): _LOGGER.error("Authentication failed") - return False + raise ConfigEntryAuthFailed("Incorrect Password") appliances_manager = AppliancesManager(backend_selector, auth) if not await appliances_manager.fetch_appliances(): diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index dbc59f82416..4a41c353d7f 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -2,7 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging +from typing import Any import aiohttp import voluptuous as vol @@ -21,6 +23,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + async def validate_input( hass: core.HomeAssistant, data: dict[str, str] @@ -46,6 +50,50 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Whirlpool Sixth Sense.""" VERSION = 1 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle re-authentication with Whirlpool Sixth Sense.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Whirlpool Sixth Sense.""" + errors: dict[str, str] = {} + + if user_input: + assert self.entry is not None + password = user_input[CONF_PASSWORD] + data = { + CONF_USERNAME: self.entry.data[CONF_USERNAME], + CONF_PASSWORD: password, + } + + try: + await validate_input(self.hass, data) + except InvalidAuth: + errors["base"] = "invalid_auth" + except (CannotConnect, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: password, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=REAUTH_SCHEMA, + errors=errors, + ) async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" diff --git a/homeassistant/components/whirlpool/translations/sk.json b/homeassistant/components/whirlpool/translations/sk.json index 5ada995aa6e..8beea53674a 100644 --- a/homeassistant/components/whirlpool/translations/sk.json +++ b/homeassistant/components/whirlpool/translations/sk.json @@ -1,7 +1,17 @@ { "config": { "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/whois/translations/de.json b/homeassistant/components/whois/translations/de.json index 3288dca40eb..8aff31a5281 100644 --- a/homeassistant/components/whois/translations/de.json +++ b/homeassistant/components/whois/translations/de.json @@ -4,10 +4,10 @@ "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "unexpected_response": "Unerwartete Antwort vom Whois-Server", - "unknown_date_format": "Unbekanntes Datumsformat in Antwort des Whois-Servers", + "unexpected_response": "Unerwartete Antwort vom Whois Server", + "unknown_date_format": "Unbekanntes Datumsformat in Antwort des Whois Servers", "unknown_tld": "Die angegebene TLD ist unbekannt oder f\u00fcr diese Integration nicht verf\u00fcgbar", - "whois_command_failed": "Whois-Befehl fehlgeschlagen: Whois-Informationen konnten nicht abgerufen werden" + "whois_command_failed": "Whois Befehl fehlgeschlagen: Whois Informationen konnten nicht abgerufen werden" }, "step": { "user": { diff --git a/homeassistant/components/whois/translations/sk.json b/homeassistant/components/whois/translations/sk.json index 102f110d9ba..3ad01750f60 100644 --- a/homeassistant/components/whois/translations/sk.json +++ b/homeassistant/components/whois/translations/sk.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "unexpected_response": "Neo\u010dak\u00e1van\u00e1 odpove\u010f zo servera whois" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wiffi/translations/sk.json b/homeassistant/components/wiffi/translations/sk.json index 892b8b2cd91..cf4a04ee532 100644 --- a/homeassistant/components/wiffi/translations/sk.json +++ b/homeassistant/components/wiffi/translations/sk.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "addr_in_use": "Port servera sa u\u017e pou\u017e\u00edva.", + "already_configured": "Port servera je u\u017e nakonfigurovan\u00fd." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wilight/support.py b/homeassistant/components/wilight/support.py index 6a03a854c70..9470bf74c8a 100644 --- a/homeassistant/components/wilight/support.py +++ b/homeassistant/components/wilight/support.py @@ -43,7 +43,7 @@ def wilight_trigger(value: Any) -> str | None: if (step == 6) & result_60: step = 7 - err_desc = "Active part shoul be less than 2" + err_desc = "Active part should be less than 2" if (step == 7) & result_2: return value diff --git a/homeassistant/components/wilight/translations/sk.json b/homeassistant/components/wilight/translations/sk.json new file mode 100644 index 00000000000..c5b9d1d505c --- /dev/null +++ b/homeassistant/components/wilight/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "not_supported_device": "Toto WiLight moment\u00e1lne nie je podporovan\u00e9", + "not_wilight_device": "Toto zariadenie nie je WiLight" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Podporovan\u00e9 s\u00fa nasleduj\u00face komponenty: {components}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index fb54c90cc1f..cb5f3c59f57 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -47,7 +47,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), SENSOR_MOISTURE: SensorEntityDescription( key=SENSOR_MOISTURE, - device_class=SENSOR_MOISTURE, + device_class=SensorDeviceClass.MOISTURE, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_LIGHT: SensorEntityDescription( diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index ad1284c7a48..fd9e1c4f352 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -18,9 +18,6 @@ "description": "\u041a\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b \u0441\u0442\u0435 \u0438\u0437\u0431\u0440\u0430\u043b\u0438 \u043d\u0430 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u0430 \u043d\u0430 Withings? \u0412\u0430\u0436\u043d\u043e \u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0438\u0442\u0435 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430\u0442, \u0432 \u043f\u0440\u043e\u0442\u0438\u0432\u0435\u043d \u0441\u043b\u0443\u0447\u0430\u0439 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0449\u0435 \u0431\u044a\u0434\u0430\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438.", "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." }, - "reauth": { - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" - }, "reauth_confirm": { "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index 91be6ffabbe..2525bfb82b4 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -24,10 +24,6 @@ "description": "Ha de proporcionar un nom de perfil \u00fanic per a aquestes dades. Normalment \u00e9s el nom del perfil seleccionat en el pas anterior.", "title": "Perfil d'usuari." }, - "reauth": { - "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", - "title": "Reautenticaci\u00f3 de la integraci\u00f3" - }, "reauth_confirm": { "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", "title": "Reautenticar la integraci\u00f3" diff --git a/homeassistant/components/withings/translations/cs.json b/homeassistant/components/withings/translations/cs.json index 06762a5fb1b..285bb39f1e7 100644 --- a/homeassistant/components/withings/translations/cs.json +++ b/homeassistant/components/withings/translations/cs.json @@ -23,9 +23,6 @@ "description": "Zadejte jedine\u010dn\u00e9 jm\u00e9no profilu pro tato data. Obvykle se jedn\u00e1 o jm\u00e9no profilu, kter\u00e9 jste vybrali v p\u0159edchoz\u00edm kroku.", "title": "U\u017eivatelsk\u00fd profil." }, - "reauth": { - "title": "Znovu ov\u011b\u0159it integraci" - }, "reauth_confirm": { "title": "Znovu ov\u011b\u0159it integraci" } diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index 672ced9ca5c..8b1bc3041e1 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -24,10 +24,6 @@ "description": "Gib einen eindeutigen Profilnamen f\u00fcr diese Daten an. Normalerweise ist dies der Name des Profils, das du im vorherigen Schritt ausgew\u00e4hlt hast.", "title": "Benutzerprofil" }, - "reauth": { - "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", - "title": "Integration erneut authentifizieren" - }, "reauth_confirm": { "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", "title": "Integration erneut authentifizieren" diff --git a/homeassistant/components/withings/translations/el.json b/homeassistant/components/withings/translations/el.json index 068d347467a..75f90e3d206 100644 --- a/homeassistant/components/withings/translations/el.json +++ b/homeassistant/components/withings/translations/el.json @@ -24,10 +24,6 @@ "description": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03b3\u03b9\u03b1 \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03c5\u03c4\u03ac. \u03a3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03c0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b1\u03c4\u03b5 \u03c3\u03c4\u03bf \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf \u03b2\u03ae\u03bc\u03b1.", "title": "\u03a0\u03c1\u03bf\u03c6\u03af\u03bb \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7." }, - "reauth": { - "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", - "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" - }, "reauth_confirm": { "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index 490e60512f9..ca969626510 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -24,10 +24,6 @@ "description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.", "title": "User Profile." }, - "reauth": { - "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", - "title": "Reauthenticate Integration" - }, "reauth_confirm": { "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", "title": "Reauthenticate Integration" diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index e3a101b1892..e49df5be40f 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -24,10 +24,6 @@ "description": "Proporciona un nombre de perfil \u00fanico para estos datos. Por lo general, este es el nombre del perfil que seleccionaste en el paso anterior.", "title": "Perfil de usuario." }, - "reauth": { - "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", - "title": "Volver a autenticar la integraci\u00f3n" - }, "reauth_confirm": { "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", "title": "Volver a autenticar la integraci\u00f3n" diff --git a/homeassistant/components/withings/translations/et.json b/homeassistant/components/withings/translations/et.json index a336b6b0bdf..b0dfa6157e1 100644 --- a/homeassistant/components/withings/translations/et.json +++ b/homeassistant/components/withings/translations/et.json @@ -24,10 +24,6 @@ "description": "Anna neile andmetele ainulaadne profiilinimi. Tavaliselt on see eelmises etapis valitud profiili nimi.", "title": "Kasutaja profiil." }, - "reauth": { - "description": "Withingi andmete jsaamiseks tuleb kasutaja {profile} taastuvastada.", - "title": "Taastuvasta sidumine" - }, "reauth_confirm": { "description": "Profiil \"{profile}\" tuleb uuesti tuvastada, et j\u00e4tkata Withingsi andmete saamist.", "title": "Taastuvasta sidumine" diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index e0d9e8db08d..18528a34330 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -24,10 +24,6 @@ "description": "Quel profil avez-vous s\u00e9lectionn\u00e9 sur le site Withings? Il est important que les profils correspondent, sinon les donn\u00e9es seront mal \u00e9tiquet\u00e9es.", "title": "Profil utilisateur" }, - "reauth": { - "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", - "title": "R\u00e9-authentifier l'int\u00e9gration" - }, "reauth_confirm": { "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", "title": "R\u00e9-authentifier l'int\u00e9gration" diff --git a/homeassistant/components/withings/translations/he.json b/homeassistant/components/withings/translations/he.json index 5624b795692..ea76011ee2d 100644 --- a/homeassistant/components/withings/translations/he.json +++ b/homeassistant/components/withings/translations/he.json @@ -13,9 +13,6 @@ "pick_implementation": { "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" }, - "reauth": { - "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" - }, "reauth_confirm": { "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index a157d5e3688..e965e742da5 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -24,10 +24,6 @@ "description": "K\u00e9rem, adjon meg egy egyedi profilnevet. Ez \u00e1ltal\u00e1ban az el\u0151z\u0151 l\u00e9p\u00e9sben kiv\u00e1lasztott profil neve.", "title": "Felhaszn\u00e1l\u00f3i profil." }, - "reauth": { - "description": "A \u201e{profile}\u201d profilt \u00fajra hiteles\u00edteni kell, hogy tov\u00e1bbra is fogadni tudja a Withings adatokat.", - "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" - }, "reauth_confirm": { "description": "A \u201e{profile}\u201d profilt \u00fajra kell hiteles\u00edteni, hogy tov\u00e1bbra is megkaphassa a Withings-adatokat.", "title": "Az integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" diff --git a/homeassistant/components/withings/translations/id.json b/homeassistant/components/withings/translations/id.json index 4b4490a617b..169a55d4cb2 100644 --- a/homeassistant/components/withings/translations/id.json +++ b/homeassistant/components/withings/translations/id.json @@ -24,10 +24,6 @@ "description": "Berikan nama profil unik untuk data ini. Umumnya namanya sama dengan nama profil yang Anda pilih di langkah sebelumnya.", "title": "Profil Pengguna." }, - "reauth": { - "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", - "title": "Autentikasi Ulang Integrasi" - }, "reauth_confirm": { "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", "title": "Autentikasi Ulang Integrasi" diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 30836be5da5..0a1ba582fb8 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -24,10 +24,6 @@ "description": "Fornire un nome di profilo univoco per questi dati. Di solito questo \u00e8 il nome del profilo selezionato nella fase precedente.", "title": "Profilo utente." }, - "reauth": { - "description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.", - "title": "Autentica nuovamente l'integrazione" - }, "reauth_confirm": { "description": "Il profilo \"{profile}\" deve essere nuovamente autenticato per continuare a ricevere i dati di Withings.", "title": "Autentica nuovamente l'integrazione" diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index 3fdcffdb918..8353c64846d 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -24,10 +24,6 @@ "description": "\u3053\u306e\u30c7\u30fc\u30bf\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)\u306a\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u901a\u5e38\u3001\u3053\u308c\u306f\u524d\u306e\u624b\u9806\u3067\u9078\u629e\u3057\u305f\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u540d\u524d\u3067\u3059\u3002", "title": "\u30e6\u30fc\u30b6\u30fc\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3002" }, - "reauth": { - "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" - }, "reauth_confirm": { "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json index 4823061de41..80eb0648e07 100644 --- a/homeassistant/components/withings/translations/ko.json +++ b/homeassistant/components/withings/translations/ko.json @@ -23,10 +23,6 @@ }, "description": "\uace0\uc720\ud55c \ud504\ub85c\ud544 \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc77c\ubc18\uc801\uc73c\ub85c \uc774\uc804 \ub2e8\uacc4\uc5d0\uc11c \uc120\ud0dd\ud55c \ud504\ub85c\ud544\uc758 \uc774\ub984\uc785\ub2c8\ub2e4.", "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." - }, - "reauth": { - "description": "Withings \ub370\uc774\ud130\ub97c \uacc4\uc18d \uc218\uc2e0\ud558\ub824\uba74 \"{profile}\" \ud504\ub85c\ud544\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" } } } diff --git a/homeassistant/components/withings/translations/lb.json b/homeassistant/components/withings/translations/lb.json index 7169517eb09..3ddf6c881ca 100644 --- a/homeassistant/components/withings/translations/lb.json +++ b/homeassistant/components/withings/translations/lb.json @@ -23,10 +23,6 @@ }, "description": "G\u00ebff een eenzegartegen Profil Numm un. Typescherweise ass dat den Numm vum Profil deens du am viirechte Schr\u00ebtt ausgewielt hues.", "title": "Benotzer Profil." - }, - "reauth": { - "description": "De Profil \"{profile}\" muss fr\u00ebsch authentifi\u00e9iert ginn fir weiderhinn Donn\u00e9e\u00eb vun Withing z'empf\u00e4nken.", - "title": "Integratioun re-authentifiz\u00e9ieren" } } } diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index 8e40180b75e..f7d9b73bc23 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -24,10 +24,6 @@ "description": "Geef een unieke profielnaam op voor deze gegevens. Meestal is dit de naam van het profiel dat u in de vorige stap hebt geselecteerd.", "title": "Gebruikersprofiel." }, - "reauth": { - "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.", - "title": "Integratie herauthenticeren" - }, "reauth_confirm": { "title": "Integratie herauthenticeren" } diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 488a43592a5..fcb8c5b8f53 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -24,10 +24,6 @@ "description": "Oppgi et unikt profilnavn for disse dataene. Dette er vanligvis navnet p\u00e5 profilen du valgte i forrige trinn.", "title": "Brukerprofil." }, - "reauth": { - "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", - "title": "Godkjenne integrering p\u00e5 nytt" - }, "reauth_confirm": { "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", "title": "Re-autentiser integrasjon" diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 27544b29fa4..efdfdb701c0 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -24,10 +24,6 @@ "description": "Podaj unikaln\u0105 nazw\u0119 profilu. Zwykle jest to nazwa profilu wybranego w poprzednim kroku.", "title": "Profil u\u017cytkownika" }, - "reauth": { - "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", - "title": "Ponownie uwierzytelnij integracj\u0119" - }, "reauth_confirm": { "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", "title": "Ponownie uwierzytelnij integracj\u0119" diff --git a/homeassistant/components/withings/translations/pt-BR.json b/homeassistant/components/withings/translations/pt-BR.json index 4ea2fd4a92c..d80209af0fa 100644 --- a/homeassistant/components/withings/translations/pt-BR.json +++ b/homeassistant/components/withings/translations/pt-BR.json @@ -24,10 +24,6 @@ "description": "Forne\u00e7a um nome de perfil exclusivo para esses dados. Normalmente, esse \u00e9 o nome do perfil selecionado na etapa anterior.", "title": "Perfil de usu\u00e1rio." }, - "reauth": { - "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings", - "title": "Reautenticar Integra\u00e7\u00e3o" - }, "reauth_confirm": { "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings.", "title": "Reautenticar Integra\u00e7\u00e3o" diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index fa97013c5c0..204a4fd2b69 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -18,9 +18,6 @@ }, "description": "Fornecer um nome de perfil \u00fanico para estes dados. Normalmente, este \u00e9 o nome do perfil que seleccionou na etapa anterior." }, - "reauth": { - "title": "Re-autenticar Perfil" - }, "reauth_confirm": { "title": "Reautenticar integra\u00e7\u00e3o" } diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index 78f8f3ec5e4..121dbeebf18 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -24,10 +24,6 @@ "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u044d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435, \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u043d\u0430 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u043c \u0448\u0430\u0433\u0435.", "title": "Withings" }, - "reauth": { - "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" - }, "reauth_confirm": { "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" diff --git a/homeassistant/components/withings/translations/sk.json b/homeassistant/components/withings/translations/sk.json new file mode 100644 index 00000000000..218c7d4bec7 --- /dev/null +++ b/homeassistant/components/withings/translations/sk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konfigur\u00e1cia profilu bola aktualizovan\u00e1.", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9 pomocou Withings." + }, + "error": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "flow_title": "{profile}", + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "profile": { + "data": { + "profile": "N\u00e1zov profilu" + } + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/sv.json b/homeassistant/components/withings/translations/sv.json index 8bf931882f4..1e437167d00 100644 --- a/homeassistant/components/withings/translations/sv.json +++ b/homeassistant/components/withings/translations/sv.json @@ -24,10 +24,6 @@ "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", "title": "Anv\u00e4ndarprofil." }, - "reauth": { - "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", - "title": "\u00c5terautenticera integration" - }, "reauth_confirm": { "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", "title": "G\u00f6r om autentiseringen f\u00f6r integrationen" diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json index 698fe41988c..11fc361c33a 100644 --- a/homeassistant/components/withings/translations/tr.json +++ b/homeassistant/components/withings/translations/tr.json @@ -24,10 +24,6 @@ "description": "Bu veriler i\u00e7in benzersiz bir profil ad\u0131 sa\u011flay\u0131n. Genellikle bu, \u00f6nceki ad\u0131mda se\u00e7ti\u011finiz profilin ad\u0131d\u0131r.", "title": "Kullan\u0131c\u0131 profili." }, - "reauth": { - "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile}", - "title": "Entegrasyonu Yeniden Do\u011frula" - }, "reauth_confirm": { "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile} \" profilinin yeniden do\u011frulanmas\u0131 gerekiyor.", "title": "Entegrasyonu Yeniden Do\u011frula" diff --git a/homeassistant/components/withings/translations/uk.json b/homeassistant/components/withings/translations/uk.json index 5efc27042b1..3ea7883fbcd 100644 --- a/homeassistant/components/withings/translations/uk.json +++ b/homeassistant/components/withings/translations/uk.json @@ -23,10 +23,6 @@ }, "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" } } } diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index 35328ea9353..b5e8b16029d 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -24,10 +24,6 @@ "description": "\u8acb\u70ba\u8cc7\u6599\u8a2d\u5b9a\u4e00\u7d44\u7368\u4e00\u7684\u500b\u4eba\u8a2d\u7f6e\u540d\u7a31\u3002\u901a\u5e38\u8207\u524d\u4e00\u6b65\u9a5f\u6240\u9078\u64c7\u4e4b\u8a2d\u7f6e\u6587\u4ef6\u540d\u7a31\u76f8\u540c\u3002", "title": "\u500b\u4eba\u8a2d\u5b9a\u3002" }, - "reauth": { - "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", - "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" - }, "reauth_confirm": { "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" diff --git a/homeassistant/components/wiz/translations/cs.json b/homeassistant/components/wiz/translations/cs.json index b6b916d5d3a..aa21c1d63ea 100644 --- a/homeassistant/components/wiz/translations/cs.json +++ b/homeassistant/components/wiz/translations/cs.json @@ -7,6 +7,7 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "no_ip": "Neplatn\u00e1 adresa IP.", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/wiz/translations/de.json b/homeassistant/components/wiz/translations/de.json index af503506c59..2fc23e92fdc 100644 --- a/homeassistant/components/wiz/translations/de.json +++ b/homeassistant/components/wiz/translations/de.json @@ -9,7 +9,7 @@ "bulb_time_out": "Es kann keine Verbindung zur Gl\u00fchbirne hergestellt werden. Vielleicht ist die Gl\u00fchbirne offline oder es wurde eine falsche IP eingegeben. Bitte schalte das Licht ein und versuche es erneut!", "cannot_connect": "Verbindung fehlgeschlagen", "no_ip": "Keine g\u00fcltige IP-Adresse.", - "no_wiz_light": "Die Gl\u00fchbirne kann nicht \u00fcber die Integration der WiZ-Plattform verbunden werden.", + "no_wiz_light": "Die Gl\u00fchbirne kann nicht \u00fcber die Integration der WiZ Plattform verbunden werden.", "unknown": "Unerwarteter Fehler" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/wiz/translations/he.json b/homeassistant/components/wiz/translations/he.json index 81b954067f7..44f40330fcd 100644 --- a/homeassistant/components/wiz/translations/he.json +++ b/homeassistant/components/wiz/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/wiz/translations/sk.json b/homeassistant/components/wiz/translations/sk.json index 641bd9c13ee..d5a802b858e 100644 --- a/homeassistant/components/wiz/translations/sk.json +++ b/homeassistant/components/wiz/translations/sk.json @@ -1,7 +1,30 @@ { "config": { "abort": { - "cannot_connect": "Nepodarilo sa pripoji\u0165" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_ip": "Neplatn\u00e1 adresa IP.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name} ({host})", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Zariadenie" + } + }, + "user": { + "data": { + "host": "IP adresa" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json index 36227a1e9bc..2025bf2f336 100644 --- a/homeassistant/components/wled/translations/it.json +++ b/homeassistant/components/wled/translations/it.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere il WLED chiamato `{name}` a Home Assistant?", - "title": "Dispositivo WLED rilevato" + "title": "Rilevato dispositivo WLED" } } }, diff --git a/homeassistant/components/wled/translations/select.sk.json b/homeassistant/components/wled/translations/select.sk.json new file mode 100644 index 00000000000..b7f43750f16 --- /dev/null +++ b/homeassistant/components/wled/translations/select.sk.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Neakt\u00edvny", + "1": "Akt\u00edvny", + "2": "K\u00fdm sa zariadenie nere\u0161tartuje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/sk.json b/homeassistant/components/wled/translations/sk.json new file mode 100644 index 00000000000..449f0ae40e4 --- /dev/null +++ b/homeassistant/components/wled/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + }, + "description": "Nastavte si WLED na integr\u00e1ciu s Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index 497f559a9de..7c9cdc2c6f3 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -4,7 +4,7 @@ "1_x_warmwasser": "1 x Warmwasser", "abgasklappe": "Abgasklappe", "absenkbetrieb": "Absenkbetrieb", - "absenkstop": "Absenkstop", + "absenkstop": "Absenkstopp", "aktiviert": "Aktiviert", "antilegionellenfunktion": "Anti-Legionellen-Funktion", "at_abschaltung": "AT Abschaltung", diff --git a/homeassistant/components/wolflink/translations/sensor.hr.json b/homeassistant/components/wolflink/translations/sensor.hr.json new file mode 100644 index 00000000000..98aa80601c8 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.hr.json @@ -0,0 +1,7 @@ +{ + "state": { + "wolflink__state": { + "permanent": "Trajno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.sk.json b/homeassistant/components/wolflink/translations/sensor.sk.json new file mode 100644 index 00000000000..0ad86ee29b6 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.sk.json @@ -0,0 +1,20 @@ +{ + "state": { + "wolflink__state": { + "aus": "Zak\u00e1zan\u00e9", + "bereit_keine_ladung": "Pripraven\u00e9, nena\u010d\u00edtava sa", + "cooling": "Chladenie", + "eco": "Eco", + "ein": "Povolen\u00e9", + "externe_deaktivierung": "Extern\u00e1 deaktiv\u00e1cia", + "fernschalter_ein": "Dia\u013ekov\u00e9 ovl\u00e1danie je povolen\u00e9", + "frostschutz": "Ochrana pred mrazom", + "gasdruck": "Tlak plynu", + "heizung": "Vykurovanie", + "initialisierung": "Inicializ\u00e1cia", + "kalibration": "Kalibr\u00e1cia", + "nur_heizgerat": "Iba kotol", + "urlaubsmodus": "Dovolenkov\u00fd re\u017eim" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sk.json b/homeassistant/components/wolflink/translations/sk.json index 5ada995aa6e..f1d6b3feeb3 100644 --- a/homeassistant/components/wolflink/translations/sk.json +++ b/homeassistant/components/wolflink/translations/sk.json @@ -1,7 +1,25 @@ { "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "device": { + "data": { + "device_name": "Zariadenie" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 97dc2c37501..fa94319772c 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.16"], + "requirements": ["holidays==0.17.2"], "codeowners": ["@fabaff"], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/homeassistant/components/ws66i/translations/bg.json b/homeassistant/components/ws66i/translations/bg.json index ba078ae5f11..a354fcc347b 100644 --- a/homeassistant/components/ws66i/translations/bg.json +++ b/homeassistant/components/ws66i/translations/bg.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" - }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/ws66i/translations/ca.json b/homeassistant/components/ws66i/translations/ca.json index 789edb86fb2..f293839739b 100644 --- a/homeassistant/components/ws66i/translations/ca.json +++ b/homeassistant/components/ws66i/translations/ca.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" - }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" diff --git a/homeassistant/components/ws66i/translations/cs.json b/homeassistant/components/ws66i/translations/cs.json index 04f18366eaf..b5a9fe10981 100644 --- a/homeassistant/components/ws66i/translations/cs.json +++ b/homeassistant/components/ws66i/translations/cs.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" - }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" diff --git a/homeassistant/components/ws66i/translations/de.json b/homeassistant/components/ws66i/translations/de.json index cab3e062d8e..8a3c2c63cb9 100644 --- a/homeassistant/components/ws66i/translations/de.json +++ b/homeassistant/components/ws66i/translations/de.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" - }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/ws66i/translations/el.json b/homeassistant/components/ws66i/translations/el.json index 4a1365c3a77..b919c07fb53 100644 --- a/homeassistant/components/ws66i/translations/el.json +++ b/homeassistant/components/ws66i/translations/el.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" - }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" diff --git a/homeassistant/components/ws66i/translations/en.json b/homeassistant/components/ws66i/translations/en.json index 30ef1e4205a..fd4b170b378 100644 --- a/homeassistant/components/ws66i/translations/en.json +++ b/homeassistant/components/ws66i/translations/en.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" diff --git a/homeassistant/components/ws66i/translations/es.json b/homeassistant/components/ws66i/translations/es.json index 075dd41ed56..59ee53b4499 100644 --- a/homeassistant/components/ws66i/translations/es.json +++ b/homeassistant/components/ws66i/translations/es.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" - }, "error": { "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" diff --git a/homeassistant/components/ws66i/translations/et.json b/homeassistant/components/ws66i/translations/et.json index 83b238d74f5..b7eef141129 100644 --- a/homeassistant/components/ws66i/translations/et.json +++ b/homeassistant/components/ws66i/translations/et.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" - }, "error": { "cannot_connect": "\u00dchendamine nurjus", "unknown": "Ootamatu t\u00f5rge" diff --git a/homeassistant/components/ws66i/translations/fr.json b/homeassistant/components/ws66i/translations/fr.json index d4d3f6e7350..173f05fedbd 100644 --- a/homeassistant/components/ws66i/translations/fr.json +++ b/homeassistant/components/ws66i/translations/fr.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" - }, "error": { "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/ws66i/translations/he.json b/homeassistant/components/ws66i/translations/he.json index fa770b28bf4..b426f27e591 100644 --- a/homeassistant/components/ws66i/translations/he.json +++ b/homeassistant/components/ws66i/translations/he.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" - }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" diff --git a/homeassistant/components/ws66i/translations/hu.json b/homeassistant/components/ws66i/translations/hu.json index 9d6585e8f6b..34d1ef39880 100644 --- a/homeassistant/components/ws66i/translations/hu.json +++ b/homeassistant/components/ws66i/translations/hu.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" - }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/ws66i/translations/id.json b/homeassistant/components/ws66i/translations/id.json index 54cb3043a11..095e17b6750 100644 --- a/homeassistant/components/ws66i/translations/id.json +++ b/homeassistant/components/ws66i/translations/id.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" - }, "error": { "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/ws66i/translations/it.json b/homeassistant/components/ws66i/translations/it.json index c98714c98ad..8aab3117ca8 100644 --- a/homeassistant/components/ws66i/translations/it.json +++ b/homeassistant/components/ws66i/translations/it.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" - }, "error": { "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/ws66i/translations/ja.json b/homeassistant/components/ws66i/translations/ja.json index 2ae21b3916c..46d7c262c3b 100644 --- a/homeassistant/components/ws66i/translations/ja.json +++ b/homeassistant/components/ws66i/translations/ja.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" - }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/ws66i/translations/ko.json b/homeassistant/components/ws66i/translations/ko.json index ba31d74c21a..33191483bdd 100644 --- a/homeassistant/components/ws66i/translations/ko.json +++ b/homeassistant/components/ws66i/translations/ko.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/ws66i/translations/nl.json b/homeassistant/components/ws66i/translations/nl.json index 0ff24636b88..5e31bbd4e76 100644 --- a/homeassistant/components/ws66i/translations/nl.json +++ b/homeassistant/components/ws66i/translations/nl.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Apparaat is al geconfigureerd" - }, "error": { "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/ws66i/translations/no.json b/homeassistant/components/ws66i/translations/no.json index fa132ab681a..ff5e34742a5 100644 --- a/homeassistant/components/ws66i/translations/no.json +++ b/homeassistant/components/ws66i/translations/no.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Enheten er allerede konfigurert" - }, "error": { "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" diff --git a/homeassistant/components/ws66i/translations/pl.json b/homeassistant/components/ws66i/translations/pl.json index 1fc8c8b1cc9..e4ca696f55b 100644 --- a/homeassistant/components/ws66i/translations/pl.json +++ b/homeassistant/components/ws66i/translations/pl.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" - }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" diff --git a/homeassistant/components/ws66i/translations/pt-BR.json b/homeassistant/components/ws66i/translations/pt-BR.json index 68bc805d08c..e50b556dbdc 100644 --- a/homeassistant/components/ws66i/translations/pt-BR.json +++ b/homeassistant/components/ws66i/translations/pt-BR.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" - }, "error": { "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" diff --git a/homeassistant/components/ws66i/translations/ru.json b/homeassistant/components/ws66i/translations/ru.json index b7e244cf2b0..aa9a8125f2b 100644 --- a/homeassistant/components/ws66i/translations/ru.json +++ b/homeassistant/components/ws66i/translations/ru.json @@ -1,8 +1,5 @@ { "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.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/ws66i/translations/sk.json b/homeassistant/components/ws66i/translations/sk.json new file mode 100644 index 00000000000..5544d1ebd9b --- /dev/null +++ b/homeassistant/components/ws66i/translations/sk.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + }, + "title": "Pripojte sa k zariadeniu" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "N\u00e1zov zdroja #1", + "source_2": "N\u00e1zov zdroja #2", + "source_3": "N\u00e1zov zdroja #3", + "source_4": "N\u00e1zov zdroja #4", + "source_5": "N\u00e1zov zdroja #5", + "source_6": "N\u00e1zov zdroja #6" + }, + "title": "Konfigur\u00e1cia zdrojov" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/sv.json b/homeassistant/components/ws66i/translations/sv.json index 2ed5262ea3d..1bd654e40a4 100644 --- a/homeassistant/components/ws66i/translations/sv.json +++ b/homeassistant/components/ws66i/translations/sv.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" - }, "error": { "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" diff --git a/homeassistant/components/ws66i/translations/tr.json b/homeassistant/components/ws66i/translations/tr.json index 5baea0cee9d..4fec56f89b8 100644 --- a/homeassistant/components/ws66i/translations/tr.json +++ b/homeassistant/components/ws66i/translations/tr.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" - }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/ws66i/translations/zh-Hant.json b/homeassistant/components/ws66i/translations/zh-Hant.json index a583ac2217f..780927c7a66 100644 --- a/homeassistant/components/ws66i/translations/zh-Hant.json +++ b/homeassistant/components/ws66i/translations/zh-Hant.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 5b9dcd77f2b..1d56cfc71c5 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -43,7 +43,7 @@ SUPPORT_XBOX = ( | MediaPlayerEntityFeature.PLAY_MEDIA ) -XBOX_STATE_MAP = { +XBOX_STATE_MAP: dict[PlaybackState | PowerState, MediaPlayerState | None] = { PlaybackState.Playing: MediaPlayerState.PLAYING, PlaybackState.Paused: MediaPlayerState.PAUSED, PowerState.On: MediaPlayerState.ON, @@ -99,7 +99,7 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit return self.coordinator.data.consoles[self._console.id] @property - def state(self): + def state(self) -> MediaPlayerState | None: """State of the player.""" status = self.data.status if status.playback_state in XBOX_STATE_MAP: @@ -107,7 +107,7 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit return XBOX_STATE_MAP[status.power_state] @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" if self.state not in [MediaPlayerState.PLAYING, MediaPlayerState.PAUSED]: return ( diff --git a/homeassistant/components/xbox/translations/sk.json b/homeassistant/components/xbox/translations/sk.json index c19b1a0b70c..732253dabb4 100644 --- a/homeassistant/components/xbox/translations/sk.json +++ b/homeassistant/components/xbox/translations/sk.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index 07adcbeb5cc..f75dbe6ba4e 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Xbox platform.""" + create_issue( + hass, + "xbox_live", + "pending_removal", + breaks_in_ha_version="2023.2.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The Xbox Live integration is deprecated " + "and will be removed in Home Assistant 2023.2" + ) api = Client(api_key=config[CONF_API_KEY]) entities = [] diff --git a/homeassistant/components/xbox_live/strings.json b/homeassistant/components/xbox_live/strings.json new file mode 100644 index 00000000000..0f73f851bd7 --- /dev/null +++ b/homeassistant/components/xbox_live/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The Xbox Live integration is being removed", + "description": "The Xbox Live integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2023.2.\n\nThe integration is being removed, because it is only useful for the legacy device Xbox 360 and the upstream API now requires a paid subscription. Newer consoles are supported by the Xbox integration for free.\n\nRemove the Xbox Live YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/xbox_live/translations/en.json b/homeassistant/components/xbox_live/translations/en.json new file mode 100644 index 00000000000..cb2ca622a22 --- /dev/null +++ b/homeassistant/components/xbox_live/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The Xbox Live integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2023.2.\n\nThe integration is being removed, because it is only useful for the legacy device Xbox 360 and the upstream API now requires a paid subscription. Newer consoles are supported by the Xbox integration for free.\n\nRemove the Xbox Live YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xbox Live integration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 3483bf2d3f8..6bef9c3d4b5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -7,7 +7,7 @@ }, "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 Xiaomi Aqara, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 HomeAssistant \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.. \u0421\u043f\u043e\u0441\u043e\u0431\u044b \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0437\u0434\u0435\u0441\u044c: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.. \u0421\u043f\u043e\u0441\u043e\u0431\u044b \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0437\u0434\u0435\u0441\u044c: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.", "invalid_interface": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0430.", "invalid_mac": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 MAC-\u0430\u0434\u0440\u0435\u0441." diff --git a/homeassistant/components/xiaomi_aqara/translations/sk.json b/homeassistant/components/xiaomi_aqara/translations/sk.json index 299acb612fb..f6fbe6ed65c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/sk.json +++ b/homeassistant/components/xiaomi_aqara/translations/sk.json @@ -3,6 +3,29 @@ "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "error": { + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa , pozrite si https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Neplatn\u00e9 sie\u0165ov\u00e9 rozhranie", + "invalid_key": "Neplatn\u00fd k\u013e\u00fa\u010d br\u00e1ny", + "invalid_mac": "Neplatn\u00e1 adresa Mac" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_ip": "IP adresa" + } + }, + "settings": { + "title": "Volite\u013en\u00e9 nastavenia" + }, + "user": { + "data": { + "host": "IP adresa (volite\u013en\u00e1)", + "mac": "Adresa Mac (volite\u013en\u00e9)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index f899600a8d1..201e4f14582 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -17,7 +17,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from .const import DOMAIN @@ -60,7 +60,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _needs_poll( service_info: BluetoothServiceInfoBleak, last_poll: float | None ) -> bool: - return data.poll_needed(service_info, last_poll) + # Only poll if hass is running, we need to poll, + # and we actually have a way to connect to the device + return ( + hass.state == CoreState.running + and data.poll_needed(service_info, last_poll) + and bool( + async_ble_device_from_address( + hass, service_info.device.address, connectable=True + ) + ) + ) async def _async_poll(service_info: BluetoothServiceInfoBleak): # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 4de491ab9dd..6fc6c3c2761 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -22,9 +22,10 @@ from homeassistant.components.bluetooth.passive_update_processor import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( @@ -51,7 +52,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/xiaomi_ble/device.py b/homeassistant/components/xiaomi_ble/device.py index 4ddfc31ae51..5714db4eadd 100644 --- a/homeassistant/components/xiaomi_ble/device.py +++ b/homeassistant/components/xiaomi_ble/device.py @@ -1,13 +1,11 @@ """Support for Xioami BLE devices.""" from __future__ import annotations -from xiaomi_ble import DeviceKey, SensorDeviceInfo +from xiaomi_ble import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, ) -from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME -from homeassistant.helpers.entity import DeviceInfo def device_key_to_bluetooth_entity_key( @@ -15,17 +13,3 @@ def device_key_to_bluetooth_entity_key( ) -> PassiveBluetoothEntityKey: """Convert a device key to an entity key.""" return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 56efd9e966a..3f02c4e8767 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -4,12 +4,16 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "bluetooth": [ + { + "connectable": false, + "service_data_uuid": "0000fd50-0000-1000-8000-00805f9b34fb" + }, { "connectable": false, "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.10.0"], + "requirements": ["xiaomi-ble==0.12.2"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 1aa1b7f8f72..831b5d0910b 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -29,16 +29,33 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN -from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { - (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( - key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (DeviceClass.CONDUCTIVITY, Units.CONDUCTIVITY): SensorEntityDescription( + key=str(Units.CONDUCTIVITY), + device_class=None, + native_unit_of_measurement=CONDUCTIVITY, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.FORMALDEHYDE, + Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{DeviceClass.FORMALDEHYDE}_{Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER}", + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( @@ -53,24 +70,18 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=LIGHT_LUX, state_class=SensorStateClass.MEASUREMENT, ), + (DeviceClass.MOISTURE, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.MOISTURE, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_MBAR, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( - key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", - device_class=SensorDeviceClass.BATTERY, - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( - key=str(Units.ELECTRIC_POTENTIAL_VOLT), - device_class=SensorDeviceClass.VOLTAGE, - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=SensorStateClass.MEASUREMENT, - ), ( DeviceClass.SIGNAL_STRENGTH, Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -80,27 +91,28 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, ), - # Used for e.g. moisture sensor on HHCCJCY01 + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=f"{DeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + # Used for e.g. consumable sensor on WX08ZM (None, Units.PERCENTAGE): SensorEntityDescription( key=str(Units.PERCENTAGE), device_class=None, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - # Used for e.g. conductivity sensor on HHCCJCY01 - (None, Units.CONDUCTIVITY): SensorEntityDescription( - key=str(Units.CONDUCTIVITY), - device_class=None, - native_unit_of_measurement=CONDUCTIVITY, - state_class=SensorStateClass.MEASUREMENT, - ), - # Used for e.g. formaldehyde - (None, Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER): SensorEntityDescription( - key=str(Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER), - native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), } @@ -110,7 +122,7 @@ def sensor_update_to_bluetooth_data_update( """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index d36daedf3a7..019d50b34ae 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", - "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", - "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", - "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals.", "no_devices_found": "No s'han trobat dispositius a la xarxa", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, @@ -34,9 +31,6 @@ }, "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals." }, - "slow_confirm": { - "description": "No s'ha em\u00e8s cap 'broadcast' des d'aquest dispositiu durant l'\u00faltim minut, per tant no estem segurs de si aquest dispositiu utilitza encriptaci\u00f3 o no. Aix\u00f2 pot ser perqu\u00e8 el dispositiu utilitza un interval de 'broadcast' lent. Confirma per afegir aquest dispositiu de totes maneres, i la pr\u00f2xima vegada que rebi un 'broadcast' se't demanar\u00e0 que introdueixis la seva clau d'enlla\u00e7 si \u00e9s necessari." - }, "user": { "data": { "address": "Dispositiu" diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index 448a160c3ab..911e4c5d9ae 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", - "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", - "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, @@ -20,7 +17,7 @@ "description": "M\u00f6chtest du {name} einrichten?" }, "confirm_slow": { - "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, so dass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." + "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, sodass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." }, "get_encryption_key_4_5": { "data": { @@ -34,9 +31,6 @@ }, "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindkey." }, - "slow_confirm": { - "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, so dass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." - }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json index e6c5efce91f..5876ca38651 100644 --- a/homeassistant/components/xiaomi_ble/translations/el.json +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", - "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", - "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", - "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, @@ -34,9 +31,6 @@ }, "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7\u03c2 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd." }, - "slow_confirm": { - "description": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b3\u03af\u03bd\u03b5\u03b9 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bc\u03b1\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03b1\u03bd \u03b1\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03ae \u03cc\u03c7\u03b9. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03c6\u03b5\u03af\u03bb\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03b1\u03c1\u03b3\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2. \u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bf\u03cd\u03c4\u03c9\u03c2 \u03ae \u03ac\u03bb\u03bb\u03c9\u03c2, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bb\u03b7\u03c6\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c4\u03b7\u03c2, \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9." - }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index be75cc007b2..2cb77dd2c07 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, @@ -34,9 +31,6 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, - "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." - }, "user": { "data": { "address": "Device" diff --git a/homeassistant/components/xiaomi_ble/translations/es.json b/homeassistant/components/xiaomi_ble/translations/es.json index cf441adbefa..a1b796cd665 100644 --- a/homeassistant/components/xiaomi_ble/translations/es.json +++ b/homeassistant/components/xiaomi_ble/translations/es.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", - "expected_24_characters": "Se esperaba una clave de enlace hexadecimal de 24 caracteres.", - "expected_32_characters": "Se esperaba una clave de enlace hexadecimal de 32 caracteres.", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, @@ -34,9 +31,6 @@ }, "description": "Los datos del sensor transmitidos por el sensor est\u00e1n cifrados. Para descifrarlos necesitamos una clave de enlace hexadecimal de 24 caracteres." }, - "slow_confirm": { - "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para agregar este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." - }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json index 41bf99207e2..49c7031a3a4 100644 --- a/homeassistant/components/xiaomi_ble/translations/et.json +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", - "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", - "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", - "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", "no_devices_found": "V\u00f6rgust seadmeid ei leitud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, @@ -34,9 +31,6 @@ }, "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." }, - "slow_confirm": { - "description": "Sellest seadmest ei ole viimasel minutil \u00fchtegi saadet olnud, nii et me ei ole kindlad, kas see seade kasutab kr\u00fcpteerimist v\u00f5i mitte. See v\u00f5ib olla tingitud sellest, et seade kasutab aeglast saateintervalli. Kinnita, et lisate selle seadme ikkagi, siis j\u00e4rgmisel korral, kui saade saabub, palutakse sisestada selle sidumisv\u00f5ti, kui seda on vaja." - }, "user": { "data": { "address": "Seade" diff --git a/homeassistant/components/xiaomi_ble/translations/he.json b/homeassistant/components/xiaomi_ble/translations/he.json index b90a366130a..0df85dd1fe5 100644 --- a/homeassistant/components/xiaomi_ble/translations/he.json +++ b/homeassistant/components/xiaomi_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "flow_title": "{name}", diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index fed82381dcb..95adee995dc 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", - "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", - "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, @@ -34,9 +31,6 @@ }, "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." }, - "slow_confirm": { - "description": "Az elm\u00falt egy percben nem \u00e9rkezett ad\u00e1sjel az eszk\u00f6zt\u0151l, \u00edgy nem az nem \u00e1llap\u00edthat\u00f3 meg egy\u00e9rtelm\u0171en, hogy ez a k\u00e9sz\u00fcl\u00e9k haszn\u00e1l-e titkos\u00edt\u00e1st vagy sem. Ez az\u00e9rt lehet, mert az eszk\u00f6z ritka jelad\u00e1si intervallumot haszn\u00e1l. Meger\u0151s\u00edtheti most az eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1t, de a k\u00f6vetkez\u0151 ad\u00e1sjel fogad\u00e1sakor a rendszer k\u00e9rni fogja, hogy adja meg az eszk\u00f6z kulcs\u00e1t (bindkeyt), ha az sz\u00fcks\u00e9ges." - }, "user": { "data": { "address": "Eszk\u00f6z" diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index e6e29966bc7..13fbccc1333 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", - "expected_24_characters": "Diharapkan bindkey berupa 24 karakter heksadesimal.", - "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal.", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "reauth_successful": "Autentikasi ulang berhasil" }, @@ -34,9 +31,6 @@ }, "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 24 karakter bindkey heksadesimal ." }, - "slow_confirm": { - "description": "Belum ada siaran dari perangkat ini dalam menit terakhir jadi kami tidak yakin apakah perangkat ini menggunakan enkripsi atau tidak. Ini mungkin terjadi karena perangkat menggunakan interval siaran yang lambat. Konfirmasikan sekarang untuk menambahkan perangkat ini, dan ketika siaran diterima nanti, Anda akan diminta untuk memasukkan kunci bind jika diperlukan." - }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json index bf5ee87b949..7d6837d35ee 100644 --- a/homeassistant/components/xiaomi_ble/translations/it.json +++ b/homeassistant/components/xiaomi_ble/translations/it.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", - "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", - "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", "no_devices_found": "Nessun dispositivo trovato sulla rete", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, @@ -34,9 +31,6 @@ }, "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 24 caratteri." }, - "slow_confirm": { - "description": "Non c'\u00e8 stata una trasmissione da questo dispositivo nell'ultimo minuto, quindi non siamo sicuri se questo dispositivo utilizzi la crittografia o meno. Ci\u00f2 potrebbe essere dovuto al fatto che il dispositivo utilizza un intervallo di trasmissione lento. Conferma per aggiungere comunque questo dispositivo, la prossima volta che viene ricevuta una trasmissione ti verr\u00e0 chiesto di inserire la sua chiave di collegamento se necessario." - }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json index 3d84421b1b3..8352df42c0c 100644 --- a/homeassistant/components/xiaomi_ble/translations/ja.json +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "expected_24_characters": "24\u6587\u5b57\u306716\u9032\u6570\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", - "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, @@ -34,9 +31,6 @@ }, "description": "\u30bb\u30f3\u30b5\u30fc\u304b\u3089\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3055\u308c\u308b\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u306f\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5fa9\u53f7\u5316\u3059\u308b\u306b\u306f\u300116\u9032\u6570\u306724\u6587\u5b57\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, - "slow_confirm": { - "description": "\u76f4\u524d\u306b\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u306e\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u304c\u306a\u304b\u3063\u305f\u305f\u3081\u3001\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u6697\u53f7\u5316\u3092\u4f7f\u7528\u3057\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u306f\u308f\u304b\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u9045\u3044\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u9593\u9694\u3092\u4f7f\u7528\u3057\u3066\u3044\u308b\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u3068\u306b\u304b\u304f\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6b21\u306b\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3092\u53d7\u4fe1\u3057\u305f\u3068\u304d\u306b\u3001\u5fc5\u8981\u306b\u5fdc\u3058\u3066\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u3092\u5165\u529b\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002" - }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/xiaomi_ble/translations/nl.json b/homeassistant/components/xiaomi_ble/translations/nl.json index 6b79e0311de..de54d8aff8c 100644 --- a/homeassistant/components/xiaomi_ble/translations/nl.json +++ b/homeassistant/components/xiaomi_ble/translations/nl.json @@ -6,11 +6,25 @@ "no_devices_found": "Geen apparaten gevonden op het netwerk", "reauth_successful": "Herauthenticatie geslaagd" }, + "error": { + "decryption_failed": "De opgegeven `bindkey` werkte niet, de sensorgegevens konden niet worden ontsleuteld. Controleer dit en probeer het opnieuw.", + "expected_24_characters": "Verwachtte een hexadecimale `bindkey` van 24 karakters.", + "expected_32_characters": "Verwachtte een hexadecimale `bindkey` van 32 karakters." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Wilt u {name} instellen?" }, + "confirm_slow": { + "description": "Er is de laatste minuut geen aankondigingsbericht van dit apparaat ontvangen, dus we weten niet zeker of dit apparaat encryptie gebruikt of niet. Dit kan zijn omdat het apparaat een trage aankodigings interval heeft. Bevestig om dit apparaat hoe dan ook toe te voegen, dan zal wanneer binnenkort een aankodiging wordt ontvangen worden gevraagd om de `bindkey` als dat nodig is." + }, + "get_encryption_key_4_5": { + "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 32 tekens nodig." + }, + "get_encryption_key_legacy": { + "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 24 tekens nodig." + }, "user": { "data": { "address": "Apparaat" diff --git a/homeassistant/components/xiaomi_ble/translations/no.json b/homeassistant/components/xiaomi_ble/translations/no.json index 46a8158cad9..3c8670c3c94 100644 --- a/homeassistant/components/xiaomi_ble/translations/no.json +++ b/homeassistant/components/xiaomi_ble/translations/no.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", - "expected_24_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 24 tegn.", - "expected_32_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 32 tegn.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "reauth_successful": "Re-autentisering var vellykket" }, @@ -34,9 +31,6 @@ }, "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 24 tegn." }, - "slow_confirm": { - "description": "Det har ikke v\u00e6rt en kringkasting fra denne enheten i siste \u00f8yeblikk, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et sakte kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, blir du bedt om \u00e5 angi bindingsn\u00f8kkelen hvis det er n\u00f8dvendig." - }, "user": { "data": { "address": "Enhet" diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json index 7bb0c5da454..e5a9d7bf3ef 100644 --- a/homeassistant/components/xiaomi_ble/translations/pl.json +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", - "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", - "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", - "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, @@ -34,9 +31,6 @@ }, "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 24-znakowego szesnastkowego klucza bindkey." }, - "slow_confirm": { - "description": "W ci\u0105gu ostatniej minuty nie by\u0142o transmisji z tego urz\u0105dzenia, wi\u0119c nie jeste\u015bmy pewni, czy to urz\u0105dzenie u\u017cywa szyfrowania, czy nie. Mo\u017ce to by\u0107 spowodowane tym, \u017ce urz\u0105dzenie u\u017cywa wolnego od\u015bwie\u017cania transmisji. Potwierd\u017a, aby mimo wszystko doda\u0107 to urz\u0105dzenie, a przy nast\u0119pnym odebraniu transmisji zostaniesz poproszony o wprowadzenie klucza bindkey, je\u015bli jest to konieczne." - }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json index a21c3e1dd9c..b9475b760f3 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", - "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", - "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, @@ -34,9 +31,6 @@ }, "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 24 caracteres." }, - "slow_confirm": { - "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua bindkey, se necess\u00e1rio." - }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/xiaomi_ble/translations/pt.json b/homeassistant/components/xiaomi_ble/translations/pt.json index 9c78f327cb9..e7b9aaefbed 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt.json +++ b/homeassistant/components/xiaomi_ble/translations/pt.json @@ -8,9 +8,6 @@ "step": { "confirm_slow": { "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua chave de liga\u00e7\u00e3o, se necess\u00e1rio." - }, - "slow_confirm": { - "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua chave de liga\u00e7\u00e3o, se necess\u00e1rio." } } } diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json index 3c0bf6cce78..069255a3d50 100644 --- a/homeassistant/components/xiaomi_ble/translations/ru.json +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -3,9 +3,6 @@ "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.", "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.", - "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", - "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", - "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", "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.", "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." }, @@ -34,9 +31,6 @@ }, "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." }, - "slow_confirm": { - "description": "\u0412 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u043c\u0438\u043d\u0443\u0442\u044b \u043e\u0442 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0431\u044b\u043b\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 \u043d\u0435\u0442. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0432\u0435\u0449\u0430\u043d\u0438\u044f. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0412\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u0435\u0433\u043e \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438, \u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c." - }, "user": { "data": { "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/xiaomi_ble/translations/sk.json b/homeassistant/components/xiaomi_ble/translations/sk.json new file mode 100644 index 00000000000..431454357f8 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/sv.json b/homeassistant/components/xiaomi_ble/translations/sv.json index 85dd1bdf557..76c41778dca 100644 --- a/homeassistant/components/xiaomi_ble/translations/sv.json +++ b/homeassistant/components/xiaomi_ble/translations/sv.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfiguration redan ig\u00e5ng", - "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", - "expected_24_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 24 tecken.", - "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken.", "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket", "reauth_successful": "\u00c5terautentisering lyckades" }, @@ -34,9 +31,6 @@ }, "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 24 tecken." }, - "slow_confirm": { - "description": "Det har inte s\u00e4nts fr\u00e5n den h\u00e4r enheten under den senaste minuten s\u00e5 vi \u00e4r inte s\u00e4kra p\u00e5 om den h\u00e4r enheten anv\u00e4nder kryptering eller inte. Det kan bero p\u00e5 att enheten anv\u00e4nder ett l\u00e5ngsamt s\u00e4ndningsintervall. Bekr\u00e4fta att l\u00e4gga till den h\u00e4r enheten \u00e4nd\u00e5, n\u00e4sta g\u00e5ng en s\u00e4ndning tas emot kommer du att uppmanas att ange dess bindnyckel om det beh\u00f6vs." - }, "user": { "data": { "address": "Enhet" diff --git a/homeassistant/components/xiaomi_ble/translations/tr.json b/homeassistant/components/xiaomi_ble/translations/tr.json index 9d1b1931e79..cd4f6f6772d 100644 --- a/homeassistant/components/xiaomi_ble/translations/tr.json +++ b/homeassistant/components/xiaomi_ble/translations/tr.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "decryption_failed": "Sa\u011flanan ba\u011flama anahtar\u0131 \u00e7al\u0131\u015fmad\u0131, sens\u00f6r verilerinin \u015fifresi \u00e7\u00f6z\u00fclemedi. L\u00fctfen kontrol edin ve tekrar deneyin.", - "expected_24_characters": "24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", - "expected_32_characters": "32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, @@ -34,9 +31,6 @@ }, "description": "Sens\u00f6r taraf\u0131ndan yay\u0131nlanan sens\u00f6r verileri \u015fifrelenmi\u015ftir. \u015eifreyi \u00e7\u00f6zmek i\u00e7in 24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131na ihtiyac\u0131m\u0131z var." }, - "slow_confirm": { - "description": "Son dakikada bu cihazdan bir yay\u0131n olmad\u0131\u011f\u0131 i\u00e7in bu cihaz\u0131n \u015fifreleme kullan\u0131p kullanmad\u0131\u011f\u0131ndan emin de\u011filiz. Bunun nedeni, cihaz\u0131n yava\u015f bir yay\u0131n aral\u0131\u011f\u0131 kullanmas\u0131 olabilir. Yine de bu cihaz\u0131 eklemeyi onaylay\u0131n, ard\u0131ndan bir sonraki yay\u0131n al\u0131nd\u0131\u011f\u0131nda gerekirse bindkey'i girmeniz istenecektir." - }, "user": { "data": { "address": "Cihaz" diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json index fdb720b2777..6faa7422e56 100644 --- a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", - "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", - "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, @@ -34,9 +31,6 @@ }, "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" }, - "slow_confirm": { - "description": "\u8a72\u88dd\u7f6e\u65bc\u904e\u53bb\u4e00\u5206\u9418\u5167\u3001\u672a\u9032\u884c\u4efb\u4f55\u72c0\u614b\u5ee3\u64ad\uff0c\u56e0\u6b64\u7121\u6cd5\u78ba\u5b9a\u88dd\u7f6e\u662f\u5426\u4f7f\u7528\u52a0\u5bc6\u901a\u8a0a\u3002\u4e5f\u53ef\u80fd\u56e0\u70ba\u88dd\u7f6e\u7684\u66f4\u65b0\u983b\u7387\u8f03\u6162\u3002\u78ba\u8a8d\u9084\u662f\u8981\u65b0\u589e\u6b64\u88dd\u7f6e\u3001\u65bc\u4e0b\u6b21\u6536\u5230\u88dd\u7f6e\u5ee3\u64ad\u6642\uff0c\u5982\u679c\u9700\u8981\u3001\u5c07\u63d0\u793a\u60a8\u8f38\u5165\u7d81\u5b9a\u78bc\u3002" - }, "user": { "data": { "address": "\u88dd\u7f6e" diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 0c090a58e02..6621e41e7aa 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -1,8 +1,13 @@ """Constants for the Xiaomi Miio component.""" -from miio.vacuum import ( +from miio.integrations.vacuum.roborock.vacuum import ( + ROCKROBO_E2, + ROCKROBO_S4, + ROCKROBO_S4_MAX, ROCKROBO_S5, + ROCKROBO_S5_MAX, ROCKROBO_S6, ROCKROBO_S6_MAXV, + ROCKROBO_S6_PURE, ROCKROBO_S7, ROCKROBO_S7_MAXV, ROCKROBO_V1, @@ -70,6 +75,7 @@ MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1" MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2" MODEL_AIRPURIFIER_V3 = "zhimi.airpurifier.v3" MODEL_AIRPURIFIER_V5 = "zhimi.airpurifier.v5" +MODEL_AIRPURIFIER_ZA1 = "zhimi.airpurifier.za1" MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" @@ -124,6 +130,7 @@ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_4_LITE_RMB1, MODEL_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO, + MODEL_AIRPURIFIER_ZA1, ] MODELS_PURIFIER_MIIO = [ MODEL_AIRPURIFIER_V1, @@ -213,12 +220,6 @@ MODELS_LIGHT = ( + MODELS_LIGHT_MONO ) -# TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme -ROCKROBO_S4 = "roborock.vacuum.s4" -ROCKROBO_S4_MAX = "roborock.vacuum.a19" -ROCKROBO_S5_MAX = "roborock.vacuum.s5e" -ROCKROBO_S6_PURE = "roborock.vacuum.a08" -ROCKROBO_E2 = "roborock.vacuum.e2" ROBOROCK_GENERIC = "roborock.vacuum" ROCKROBO_GENERIC = "rockrobo.vacuum" MODELS_VACUUM = [ @@ -397,6 +398,10 @@ FEATURE_FLAGS_AIRPURIFIER_V3 = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED ) +FEATURE_FLAGS_AIRPURIFIER_ZA1 = ( + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_FAVORITE_LEVEL +) + FEATURE_FLAGS_AIRHUMIDIFIER = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_TARGET_HUMIDITY ) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index dbc8c7a66d9..7153247e7d2 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -55,6 +55,7 @@ from .const import ( FEATURE_FLAGS_AIRPURIFIER_PRO, FEATURE_FLAGS_AIRPURIFIER_PRO_V7, FEATURE_FLAGS_AIRPURIFIER_V3, + FEATURE_FLAGS_AIRPURIFIER_ZA1, FEATURE_FLAGS_FAN, FEATURE_FLAGS_FAN_1C, FEATURE_FLAGS_FAN_P5, @@ -77,6 +78,7 @@ from .const import ( MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_ZA1, MODEL_FAN_1C, MODEL_FAN_P5, MODEL_FAN_P9, @@ -160,6 +162,7 @@ PRESET_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"] PRESET_MODES_AIRPURIFIER_PRO_V7 = PRESET_MODES_AIRPURIFIER_PRO PRESET_MODES_AIRPURIFIER_2S = ["Auto", "Silent", "Favorite"] PRESET_MODES_AIRPURIFIER_3C = ["Auto", "Silent", "Favorite"] +PRESET_MODES_AIRPURIFIER_ZA1 = ["Auto", "Silent", "Favorite"] PRESET_MODES_AIRPURIFIER_V3 = [ "Auto", "Silent", @@ -449,6 +452,12 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): self._preset_modes = PRESET_MODES_AIRPURIFIER_2S self._attr_supported_features = FanEntityFeature.PRESET_MODE self._speed_count = 1 + elif self._model == MODEL_AIRPURIFIER_ZA1: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_ZA1 + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT + self._preset_modes = PRESET_MODES_AIRPURIFIER_ZA1 + self._attr_supported_features = FanEntityFeature.PRESET_MODE + self._speed_count = 1 elif self._model in MODELS_PURIFIER_MIOT: self._device_features = FEATURE_FLAGS_AIRPURIFIER_MIOT self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index b5a5e738ea0..2cf3944bb91 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -115,7 +115,6 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES - supported_features: int def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 7c5439d8d35..869379530fc 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -39,6 +39,7 @@ from .const import ( FEATURE_FLAGS_AIRPURIFIER_PRO_V7, FEATURE_FLAGS_AIRPURIFIER_V1, FEATURE_FLAGS_AIRPURIFIER_V3, + FEATURE_FLAGS_AIRPURIFIER_ZA1, FEATURE_FLAGS_FAN, FEATURE_FLAGS_FAN_1C, FEATURE_FLAGS_FAN_P5, @@ -73,6 +74,7 @@ from .const import ( MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_ZA1, MODEL_FAN_1C, MODEL_FAN_P5, MODEL_FAN_P9, @@ -249,6 +251,7 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_4_LITE_RMB1: FEATURE_FLAGS_AIRPURIFIER_4_LITE, MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, + MODEL_AIRPURIFIER_ZA1: FEATURE_FLAGS_AIRPURIFIER_ZA1, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11, MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11, diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 118f3cd5c77..87cafe3c58a 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field +import logging from typing import NamedTuple from miio.fan_common import LedBrightness as FanLedBrightness @@ -52,6 +53,7 @@ from .const import ( MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2, MODEL_AIRPURIFIER_PROH, + MODEL_AIRPURIFIER_ZA1, MODEL_FAN_SA1, MODEL_FAN_V2, MODEL_FAN_V3, @@ -66,6 +68,9 @@ ATTR_LED_BRIGHTNESS = "led_brightness" ATTR_PTC_LEVEL = "ptc_level" +_LOGGER = logging.getLogger(__name__) + + @dataclass class XiaomiMiioSelectDescription(SelectEntityDescription): """A class that describes select entities.""" @@ -109,6 +114,9 @@ MODEL_TO_ATTR_MAP: dict[str, list] = { MODEL_AIRPURIFIER_3: [ AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) ], + MODEL_AIRPURIFIER_ZA1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], MODEL_AIRPURIFIER_3H: [ AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) ], @@ -248,11 +256,17 @@ class XiaomiGenericSelector(XiaomiSelector): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - attr = self._enum_class( - self._extract_value_from_attribute( + try: + value = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.attr_name ) - ) + attr = self._enum_class(value) + except ValueError: # if the value does not exist in + _LOGGER.debug( + "Value '%s' does not exist in enum %s", value, self._enum_class + ) + attr = None + if attr is not None: self._current_attr = attr self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 56938a4bd34..a83f3ef528d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -70,6 +70,7 @@ from .const import ( MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V2, MODEL_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_ZA1, MODEL_FAN_P5, MODEL_FAN_V2, MODEL_FAN_V3, @@ -97,6 +98,7 @@ UNIT_LUMEN = "lm" ATTR_ACTUAL_SPEED = "actual_speed" ATTR_AIR_QUALITY = "air_quality" +ATTR_TVOC = "tvoc" ATTR_AQI = "aqi" ATTR_BATTERY = "battery" ATTR_CARBON_DIOXIDE = "co2" @@ -262,6 +264,13 @@ SENSOR_TYPES = { icon="mdi:cloud", state_class=SensorStateClass.MEASUREMENT, ), + ATTR_TVOC: XiaomiMiioSensorDescription( + key=ATTR_TVOC, + name="TVOC", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + ), ATTR_PM10: XiaomiMiioSensorDescription( key=ATTR_PM10, name="PM10", @@ -452,6 +461,15 @@ PURIFIER_3C_SENSORS = ( ATTR_MOTOR_SPEED, ATTR_PM25, ) +PURIFIER_ZA1_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_TVOC, + ATTR_HUMIDITY, + ATTR_TEMPERATURE, +) PURIFIER_V2_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_USE, @@ -548,6 +566,7 @@ MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRPURIFIER_PRO_V7: PURIFIER_PRO_V7_SENSORS, MODEL_AIRPURIFIER_V2: PURIFIER_V2_SENSORS, MODEL_AIRPURIFIER_V3: PURIFIER_V3_SENSORS, + MODEL_AIRPURIFIER_ZA1: PURIFIER_ZA1_SENSORS, MODEL_FAN_V2: FAN_V2_V3_SENSORS, MODEL_FAN_V3: FAN_V2_V3_SENSORS, MODEL_FAN_ZA5: FAN_ZA5_SENSORS, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 2f45ba0adca..f2b7b071923 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -53,6 +53,7 @@ from .const import ( FEATURE_FLAGS_AIRPURIFIER_PRO_V7, FEATURE_FLAGS_AIRPURIFIER_V1, FEATURE_FLAGS_AIRPURIFIER_V3, + FEATURE_FLAGS_AIRPURIFIER_ZA1, FEATURE_FLAGS_FAN, FEATURE_FLAGS_FAN_1C, FEATURE_FLAGS_FAN_P5, @@ -90,6 +91,7 @@ from .const import ( MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_ZA1, MODEL_FAN_1C, MODEL_FAN_P5, MODEL_FAN_P9, @@ -204,6 +206,7 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_4_LITE_RMB1: FEATURE_FLAGS_AIRPURIFIER_4_LITE, MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, + MODEL_AIRPURIFIER_ZA1: FEATURE_FLAGS_AIRPURIFIER_ZA1, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11, MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11, diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json index 2ad6a9dda26..606f160f7e9 100644 --- a/homeassistant/components/xiaomi_miio/translations/bg.json +++ b/homeassistant/components/xiaomi_miio/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json index 858546d915f..7abed174b7a 100644 --- a/homeassistant/components/xiaomi_miio/translations/cs.json +++ b/homeassistant/components/xiaomi_miio/translations/cs.json @@ -5,7 +5,8 @@ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "incomplete_info": "Ne\u00fapln\u00e9 informace pro nastaven\u00ed za\u0159\u00edzen\u00ed, chyb\u00ed hostitel nebo token.", "not_xiaomi_miio": "Za\u0159\u00edzen\u00ed (zat\u00edm) nen\u00ed podporov\u00e1no integrac\u00ed Xiaomi Miio.", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/xiaomi_miio/translations/el.json b/homeassistant/components/xiaomi_miio/translations/el.json index 48f55af9246..779d71d14ff 100644 --- a/homeassistant/components/xiaomi_miio/translations/el.json +++ b/homeassistant/components/xiaomi_miio/translations/el.json @@ -5,7 +5,8 @@ "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "incomplete_info": "\u0395\u03bb\u03bb\u03b9\u03c0\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2, \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c3\u03c7\u03b5\u03b8\u03b5\u03af \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ad\u03b1\u03c2 \u03ae \u03ba\u03bf\u03c5\u03c0\u03cc\u03bd\u03b9.", "not_xiaomi_miio": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 (\u03b1\u03ba\u03cc\u03bc\u03b1) \u03b1\u03c0\u03cc \u03c4\u03bf Xiaomi Miio.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/hangouts/translations/nb.json b/homeassistant/components/xiaomi_miio/translations/hr.json similarity index 50% rename from homeassistant/components/hangouts/translations/nb.json rename to homeassistant/components/xiaomi_miio/translations/hr.json index 11a4fc139b8..be104f26c77 100644 --- a/homeassistant/components/hangouts/translations/nb.json +++ b/homeassistant/components/xiaomi_miio/translations/hr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "unknown": "Uventet feil" + "unknown": "Neo\u010dekivana gre\u0161ka" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json index 8d1d70642aa..f3dbc9877dd 100644 --- a/homeassistant/components/xiaomi_miio/translations/id.json +++ b/homeassistant/components/xiaomi_miio/translations/id.json @@ -5,7 +5,8 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "incomplete_info": "Informasi tidak lengkap untuk menyiapkan perangkat, tidak ada host atau token yang disediakan.", "not_xiaomi_miio": "Perangkat (masih) tidak didukung oleh Xiaomi Miio.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", @@ -36,7 +37,7 @@ "host": "Alamat IP", "token": "Token API" }, - "description": "Anda akan membutuhkan Token API 32 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Perhatikan bahwa Token API ini berbeda dengan kunci yang digunakan untuk integrasi Xiaomi Aqara." + "description": "Anda akan membutuhkan Token API 32 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Perhatikan bahwa Token API ini berbeda dengan kunci yang digunakan untuk integrasi Xiaomi Aqara." }, "reauth_confirm": { "description": "Integrasi Xiaomi Miio perlu mengautentikasi ulang akun Anda untuk memperbarui token atau menambahkan kredensial cloud yang hilang.", diff --git a/homeassistant/components/xiaomi_miio/translations/select.nl.json b/homeassistant/components/xiaomi_miio/translations/select.nl.json index 8041e47ab3e..71ffb1a02be 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.nl.json +++ b/homeassistant/components/xiaomi_miio/translations/select.nl.json @@ -1,12 +1,13 @@ { "state": { "xiaomi_miio__display_orientation": { + "forward": "Vooruit", "left": "Links", "right": "Rechts" }, "xiaomi_miio__led_brightness": { "bright": "Helder", - "dim": "Dim", + "dim": "Dimmen", "off": "Uit" }, "xiaomi_miio__ptc_level": { diff --git a/homeassistant/components/xiaomi_miio/translations/select.sk.json b/homeassistant/components/xiaomi_miio/translations/select.sk.json index 8745c700fe6..e2021b4e247 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.sk.json +++ b/homeassistant/components/xiaomi_miio/translations/select.sk.json @@ -1,6 +1,12 @@ { "state": { + "xiaomi_miio__display_orientation": { + "left": "V\u013eavo", + "right": "Vpravo" + }, "xiaomi_miio__led_brightness": { + "bright": "Jas", + "dim": "Dim", "off": "Vypnut\u00e9" } } diff --git a/homeassistant/components/xiaomi_miio/translations/sk.json b/homeassistant/components/xiaomi_miio/translations/sk.json index fca4223f4ac..6c9d42328fa 100644 --- a/homeassistant/components/xiaomi_miio/translations/sk.json +++ b/homeassistant/components/xiaomi_miio/translations/sk.json @@ -3,13 +3,54 @@ "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "incomplete_info": "Ne\u00fapln\u00e9 inform\u00e1cie na nastavenie zariadenia, \u017eiadny hostite\u013e alebo token.", + "not_xiaomi_miio": "Zariadenie nie je (zatia\u013e) podporovan\u00e9 spolo\u010dnos\u0165ou Xiaomi Miio.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown_device": "Model zariadenia nie je zn\u00e1my, nie je mo\u017en\u00e9 nastavi\u0165 zariadenie pomocou konfigura\u010dn\u00e9ho toku.", + "wrong_token": "Chyba kontroln\u00e9ho s\u00fa\u010dtu, nespr\u00e1vny token" + }, + "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Krajina cloudov\u00e9ho servera", + "cloud_password": "Heslo do cloudu", + "cloud_username": "Cloudov\u00e9 pou\u017e\u00edvate\u013esk\u00e9 meno", + "manual": "Konfigurova\u0165 manu\u00e1lne (neodpor\u00fa\u010da sa)" + } + }, + "connect": { + "data": { + "model": "Model zariadenia" + } + }, "manual": { "data": { + "host": "IP adresa", "token": "API token" } + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "select": { + "data": { + "select_device": "Zariadenie Miio" + }, + "description": "Vyberte zariadenie Xiaomi Miio, ktor\u00e9 chcete nastavi\u0165." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "cloud_subdevices": "Pou\u017e\u00edvanie cloudu na z\u00edskanie pripojen\u00fdch podzariaden\u00ed" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index 8c81f08ee9f..ebd9ce377c8 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -5,7 +5,8 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "incomplete_info": "Kurulum cihaz\u0131 i\u00e7in eksik bilgi, ana bilgisayar veya anahtar sa\u011flanmad\u0131.", "not_xiaomi_miio": "Cihaz (hen\u00fcz) Xiaomi Miio taraf\u0131ndan desteklenmiyor.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index 9b9971f9568..5e045d1404a 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -58,6 +58,7 @@ def setup_platform( class XiaomiTV(MediaPlayerEntity): """Represent the Xiaomi TV for Home Assistant.""" + _attr_assumed_state = True _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.TURN_ON @@ -70,23 +71,8 @@ class XiaomiTV(MediaPlayerEntity): # Initialize the Xiaomi TV. self._tv = pymitv.TV(ip) # Default name value, only to be overridden by user. - self._name = name - self._state = MediaPlayerState.OFF - - @property - def name(self): - """Return the display name of this TV.""" - return self._name - - @property - def state(self): - """Return _state variable, containing the appropriate constant.""" - return self._state - - @property - def assumed_state(self): - """Indicate that state is assumed.""" - return True + self._attr_name = name + self._attr_state = MediaPlayerState.OFF def turn_off(self) -> None: """ @@ -96,17 +82,17 @@ class XiaomiTV(MediaPlayerEntity): because the TV won't accept any input when turned off. Thus, the user would be unable to turn the TV back on, unless it's done manually. """ - if self._state != MediaPlayerState.OFF: + if self.state != MediaPlayerState.OFF: self._tv.sleep() - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF def turn_on(self) -> None: """Wake the TV back up from sleep.""" - if self._state != MediaPlayerState.ON: + if self.state != MediaPlayerState.ON: self._tv.wake() - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON def volume_up(self) -> None: """Increase volume by one.""" diff --git a/homeassistant/components/yale_smart_alarm/translations/bg.json b/homeassistant/components/yale_smart_alarm/translations/bg.json index ecab0c9bc29..8237e72bb8d 100644 --- a/homeassistant/components/yale_smart_alarm/translations/bg.json +++ b/homeassistant/components/yale_smart_alarm/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/yale_smart_alarm/translations/de.json b/homeassistant/components/yale_smart_alarm/translations/de.json index 8f24160663f..f05e54f6ffe 100644 --- a/homeassistant/components/yale_smart_alarm/translations/de.json +++ b/homeassistant/components/yale_smart_alarm/translations/de.json @@ -11,7 +11,7 @@ "step": { "reauth_confirm": { "data": { - "area_id": "Area ID", + "area_id": "Bereich-ID", "name": "Name", "password": "Passwort", "username": "Benutzername" diff --git a/homeassistant/components/yale_smart_alarm/translations/sk.json b/homeassistant/components/yale_smart_alarm/translations/sk.json index 6130380de31..e86404bb51d 100644 --- a/homeassistant/components/yale_smart_alarm/translations/sk.json +++ b/homeassistant/components/yale_smart_alarm/translations/sk.json @@ -1,21 +1,40 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "reauth_confirm": { "data": { + "area_id": "ID oblasti", "name": "N\u00e1zov", - "password": "Heslo" + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } }, "user": { "data": { - "name": "N\u00e1zov" + "area_id": "ID oblasti", + "name": "N\u00e1zov", + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "K\u00f3d nezodpoved\u00e1 po\u017eadovan\u00e9mu po\u010dtu \u010d\u00edslic" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Po\u010det \u010d\u00edslic v PIN k\u00f3de pre z\u00e1mky" } } } diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 7a2b3146265..fcecec19e6b 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -2,17 +2,15 @@ from __future__ import annotations import asyncio -from typing import TypedDict import async_timeout from yalexs_ble import PushLock, local_name_is_unique from homeassistant.components import bluetooth -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import discovery_flow from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN from .models import YaleXSBLEData @@ -21,27 +19,6 @@ from .util import async_find_existing_service_info, bluetooth_callback_matcher PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR] -class YaleXSBLEDiscovery(TypedDict): - """A validated discovery of a Yale XS BLE device.""" - - name: str - address: str - serial: str - key: str - slot: int - - -@callback -def async_discovery(hass: HomeAssistant, discovery: YaleXSBLEDiscovery) -> None: - """Update keys for the yalexs-ble integration if available.""" - discovery_flow.async_create_flow( - hass, - DOMAIN, - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data=discovery, - ) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yale Access Bluetooth from a config entry.""" local_name = entry.data[CONF_LOCAL_NAME] diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index b43ce18a7e9..ce86f7f7ea1 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.9.5"], + "requirements": ["yalexs-ble==1.10.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/homeassistant/components/yalexs_ble/translations/de.json b/homeassistant/components/yalexs_ble/translations/de.json index 416761bd91a..3fffdc25cf3 100644 --- a/homeassistant/components/yalexs_ble/translations/de.json +++ b/homeassistant/components/yalexs_ble/translations/de.json @@ -21,7 +21,7 @@ "user": { "data": { "address": "Bluetooth-Adresse", - "key": "Offline-Schl\u00fcssel (32-Byte-Hex-String)", + "key": "Offline-Schl\u00fcssel (32-Byte-Hex-Zeichenfolge)", "slot": "Offline-Schl\u00fcssel-Slot (Ganzzahl zwischen 0 und 255)" }, "description": "Lies in der Dokumentation nach, wie du den Offline-Schl\u00fcssel finden kannst." diff --git a/homeassistant/components/yalexs_ble/translations/he.json b/homeassistant/components/yalexs_ble/translations/he.json index a447b36c3ec..5ca18ddcb96 100644 --- a/homeassistant/components/yalexs_ble/translations/he.json +++ b/homeassistant/components/yalexs_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/yalexs_ble/translations/sk.json b/homeassistant/components/yalexs_ble/translations/sk.json new file mode 100644 index 00000000000..09746a52f33 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/sk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "no_unconfigured_devices": "Nena\u0161li sa \u017eiadne nenakonfigurovan\u00e9 zariadenia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_key_format": "Offline k\u013e\u00fa\u010d mus\u00ed by\u0165 32-bajtov\u00fd hexadecim\u00e1lny re\u0165azec.", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Chcete nastavi\u0165 {name} cez Bluetooth s adresou {address}?" + }, + "user": { + "data": { + "address": "Adresa Bluetooth", + "key": "Offline k\u013e\u00fa\u010d (32-bajtov\u00fd hexadecim\u00e1lny re\u0165azec)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 7d98ae5d62a..86bb0b11ec0 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -193,13 +193,9 @@ class YamahaDevice(MediaPlayerEntity): def __init__(self, name, receiver, source_ignore, source_names, zone_names): """Initialize the Yamaha Receiver.""" self.receiver = receiver - self._muted = False - self._volume = 0 - self._pwstate = MediaPlayerState.OFF - self._current_source = None - self._sound_mode = None - self._sound_mode_list = None - self._source_list = None + self._attr_is_volume_muted = False + self._attr_volume_level = 0 + self._attr_state = MediaPlayerState.OFF self._source_ignore = source_ignore or [] self._source_names = source_names or {} self._zone_names = zone_names or {} @@ -220,33 +216,33 @@ class YamahaDevice(MediaPlayerEntity): if self.receiver.on: if self._play_status is None: - self._pwstate = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON elif self._play_status.playing: - self._pwstate = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING else: - self._pwstate = MediaPlayerState.IDLE + self._attr_state = MediaPlayerState.IDLE else: - self._pwstate = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF - self._muted = self.receiver.mute - self._volume = (self.receiver.volume / 100) + 1 + self._attr_is_volume_muted = self.receiver.mute + self._attr_volume_level = (self.receiver.volume / 100) + 1 if self.source_list is None: self.build_source_list() current_source = self.receiver.input - self._current_source = self._source_names.get(current_source, current_source) + self._attr_source = self._source_names.get(current_source, current_source) self._playback_support = self.receiver.get_playback_support() self._is_playback_supported = self.receiver.is_playback_supported( - self._current_source + self._attr_source ) surround_programs = self.receiver.surround_programs() if surround_programs: - self._sound_mode = self.receiver.surround_program - self._sound_mode_list = surround_programs + self._attr_sound_mode = self.receiver.surround_program + self._attr_sound_mode_list = surround_programs else: - self._sound_mode = None - self._sound_mode_list = None + self._attr_sound_mode = None + self._attr_sound_mode_list = None def build_source_list(self): """Build the source list.""" @@ -254,7 +250,7 @@ class YamahaDevice(MediaPlayerEntity): alias: source for source, alias in self._source_names.items() } - self._source_list = sorted( + self._attr_source_list = sorted( self._source_names.get(source, source) for source in self.receiver.inputs() if source not in self._source_ignore @@ -270,48 +266,13 @@ class YamahaDevice(MediaPlayerEntity): name += f" {zone_name.replace('_', ' ')}" return name - @property - def state(self): - """Return the state of the device.""" - return self._pwstate - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def source(self): - """Return the current input source.""" - return self._current_source - - @property - def sound_mode(self): - """Return the current sound mode.""" - return self._sound_mode - - @property - def sound_mode_list(self): - """Return the current sound mode.""" - return self._sound_mode_list - - @property - def source_list(self): - """List of available input sources.""" - return self._source_list - @property def zone_id(self): """Return a zone_id to ensure 1 media player per zone.""" return f"{self.receiver.ctrl_url}:{self._zone}" @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" supported_features = SUPPORT_YAMAHA @@ -347,7 +308,7 @@ class YamahaDevice(MediaPlayerEntity): def turn_on(self) -> None: """Turn the media player on.""" self.receiver.on = True - self._volume = (self.receiver.volume / 100) + 1 + self._attr_volume_level = (self.receiver.volume / 100) + 1 def media_play(self) -> None: """Send play command.""" diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 504c56d73ec..0a6203eddf2 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -145,7 +145,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return None @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the player.""" if self.coordinator.data.zones[self._zone_id].power == "on": if self._is_netusb and self.coordinator.data.netusb_playback == "pause": @@ -424,7 +424,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): ) @property - def supported_features(self): + def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" supported_features = MUSIC_PLAYER_BASE_SUPPORT zone = self.coordinator.data.zones[self._zone_id] diff --git a/homeassistant/components/yamaha_musiccast/translations/select.cs.json b/homeassistant/components/yamaha_musiccast/translations/select.cs.json index c416256dd99..8217b2f9128 100644 --- a/homeassistant/components/yamaha_musiccast/translations/select.cs.json +++ b/homeassistant/components/yamaha_musiccast/translations/select.cs.json @@ -1,5 +1,10 @@ { "state": { + "yamaha_musiccast__zone_sleep": { + "30 min": "30 minut", + "60 min": "60 minut", + "90 min": "90 minut" + }, "yamaha_musiccast__zone_surr_decoder_type": { "toggle": "P\u0159epnout" }, diff --git a/homeassistant/components/yamaha_musiccast/translations/select.sk.json b/homeassistant/components/yamaha_musiccast/translations/select.sk.json new file mode 100644 index 00000000000..1e344784685 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.sk.json @@ -0,0 +1,14 @@ +{ + "state": { + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Komprimovan\u00e9", + "uncompressed": "Nekomprimovan\u00e9" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 min\u00fat", + "30 min": "30 min\u00fat", + "60 min": "60 min\u00fat", + "90 min": "90 min\u00fat" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/sk.json b/homeassistant/components/yamaha_musiccast/translations/sk.json index f74ba4b46d2..94a606fc1fe 100644 --- a/homeassistant/components/yamaha_musiccast/translations/sk.json +++ b/homeassistant/components/yamaha_musiccast/translations/sk.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9" + "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9", + "yxc_control_url_missing": "Riadiaca adresa URL nie je uveden\u00e1 v popise ssdp." + }, + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index af1c3de74e7..da79a1eb2d6 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1002,9 +1002,9 @@ class YeelightNightLightMode(YeelightGenericLight): return PowerMode.MOONLIGHT @property - def supported_features(self) -> int: + def supported_features(self) -> LightEntityFeature: """Flag no supported features.""" - return 0 + return LightEntityFeature(0) class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode): diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 6c450548135..f3d9d8b24ca 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.3"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/components/yeelight/translations/bg.json b/homeassistant/components/yeelight/translations/bg.json index 4a962bbf3d0..ceac6288f03 100644 --- a/homeassistant/components/yeelight/translations/bg.json +++ b/homeassistant/components/yeelight/translations/bg.json @@ -5,7 +5,7 @@ "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "{model} {id} ({host})", "step": { @@ -25,7 +25,7 @@ "step": { "init": { "data": { - "model": "\u041c\u043e\u0434\u0435\u043b (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + "model": "\u041c\u043e\u0434\u0435\u043b" } } } diff --git a/homeassistant/components/yeelight/translations/he.json b/homeassistant/components/yeelight/translations/he.json index 94e0e87d87a..bb11e283db3 100644 --- a/homeassistant/components/yeelight/translations/he.json +++ b/homeassistant/components/yeelight/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/yeelight/translations/sk.json b/homeassistant/components/yeelight/translations/sk.json index 793f8eff278..62861427cf7 100644 --- a/homeassistant/components/yeelight/translations/sk.json +++ b/homeassistant/components/yeelight/translations/sk.json @@ -1,7 +1,36 @@ { "config": { "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{model} {id} ({host})", + "step": { + "discovery_confirm": { + "description": "Chcete nastavi\u0165 {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Zariadenie" + } + }, + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 06e6fd6472a..58407df38a3 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -28,6 +28,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.COVER, + Platform.LIGHT, Platform.LOCK, Platform.SENSOR, Platform.SIREN, diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index f6add984dc2..c36f01bd4fd 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -16,6 +16,7 @@ ATTR_DEVICE_ID = "deviceId" ATTR_DEVICE_DOOR_SENSOR = "DoorSensor" ATTR_DEVICE_TH_SENSOR = "THSensor" ATTR_DEVICE_MOTION_SENSOR = "MotionSensor" +ATTR_DEVICE_MULTI_OUTLET = "MultiOutlet" ATTR_DEVICE_LEAK_SENSOR = "LeakSensor" ATTR_DEVICE_VIBRATION_SENSOR = "VibrationSensor" ATTR_DEVICE_OUTLET = "Outlet" @@ -25,3 +26,4 @@ ATTR_DEVICE_MANIPULATOR = "Manipulator" ATTR_DEVICE_CO_SMOKE_SENSOR = "COSmokeSensor" ATTR_DEVICE_SWITCH = "Switch" ATTR_DEVICE_THERMOSTAT = "Thermostat" +ATTR_DEVICE_DIMMER = "Dimmer" diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 02f063a282a..0ae960d726f 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import abstractmethod +from yolink.client_request import ClientRequest from yolink.exception import YoLinkAuthFailError, YoLinkClientError from homeassistant.config_entries import ConfigEntry @@ -70,3 +71,15 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): except YoLinkClientError as yl_client_err: self.coordinator.last_update_success = False raise HomeAssistantError(yl_client_err) from yl_client_err + + async def call_device(self, request: ClientRequest) -> None: + """Call device api.""" + try: + # call_device will check result, fail by raise YoLinkClientError + await self.coordinator.device.call_device(request) + except YoLinkAuthFailError as yl_auth_err: + self.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError(yl_auth_err) from yl_auth_err + except YoLinkClientError as yl_client_err: + self.coordinator.last_update_success = False + raise HomeAssistantError(yl_client_err) from yl_client_err diff --git a/homeassistant/components/yolink/light.py b/homeassistant/components/yolink/light.py new file mode 100644 index 00000000000..fbb8c8d50c6 --- /dev/null +++ b/homeassistant/components/yolink/light.py @@ -0,0 +1,75 @@ +"""YoLink Dimmer.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATORS, ATTR_DEVICE_DIMMER, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink Dimmer from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + entities = [ + YoLinkDimmerEntity(config_entry, device_coordinator) + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type == ATTR_DEVICE_DIMMER + ] + async_add_entities(entities) + + +class YoLinkDimmerEntity(YoLinkEntity, LightEntity): + """YoLink Dimmer Entity.""" + + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_has_entity_name = True + _attr_name = None + _attr_supported_color_modes: set[ColorMode] = {ColorMode.BRIGHTNESS} + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + ) -> None: + """Init YoLink Dimmer entity.""" + super().__init__(config_entry, coordinator) + self._attr_unique_id = f"{coordinator.device.device_id}" + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + if (dimmer_is_on := state.get("state")) is not None: + # update _attr_is_on when device report it's state + self._attr_is_on = dimmer_is_on + if (brightness := state.get("brightness")) is not None: + self._attr_brightness = round(255 * brightness / 100) + self.async_write_ha_state() + + async def toggle_light_state(self, state: str, brightness: int | None) -> None: + """Toggle light state.""" + params: dict[str, Any] = {"state": state} + if brightness is not None: + self._attr_brightness = brightness + params["brightness"] = round(brightness / 255, 2) * 100 + await self.call_device_api("setState", params) + self._attr_is_on = state == "open" + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on light.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + await self.toggle_light_state("open", brightness) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off light.""" + await self.toggle_light_state("close", None) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 665b17d9f22..3a2a1f6a390 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.1.0"], + "requirements": ["yolink-api==0.1.5"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 6a7c7ea4cff..4b658246bc9 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -13,8 +13,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import percentage @@ -48,6 +53,7 @@ class YoLinkSensorEntityDescription( """YoLink SensorEntityDescription.""" value: Callable = lambda state: state + should_update_entity: Callable = lambda state: True SENSOR_DEVICE_TYPE = [ @@ -72,6 +78,12 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_CO_SMOKE_SENSOR, ] +MCU_DEV_TEMPERATURE_SENSOR = [ + ATTR_DEVICE_LEAK_SENSOR, + ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_CO_SMOKE_SENSOR, +] + def cvt_battery(val: int | None) -> int | None: """Convert battery to percentage.""" @@ -103,11 +115,32 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( YoLinkSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, name="Temperature", state_class=SensorStateClass.MEASUREMENT, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_TH_SENSOR], ), + # mcu temperature + YoLinkSensorEntityDescription( + key="devTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + name="Temperature", + state_class=SensorStateClass.MEASUREMENT, + exists_fn=lambda device: device.device_type in MCU_DEV_TEMPERATURE_SENSOR, + should_update_entity=lambda value: value is not None, + ), + YoLinkSensorEntityDescription( + key="loraInfo", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + name="Signal", + value=lambda value: value["signal"] if value is not None else None, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + should_update_entity=lambda value: value is not None, + ), ) @@ -161,7 +194,11 @@ class YoLinkSensorEntity(YoLinkEntity, SensorEntity): @callback def update_entity_state(self, state: dict) -> None: """Update HA Entity State.""" - self._attr_native_value = self.entity_description.value( - state.get(self.entity_description.key) - ) + if ( + attr_val := self.entity_description.value( + state.get(self.entity_description.key) + ) + ) is None and self.entity_description.should_update_entity(attr_val) is False: + return + self._attr_native_value = attr_val self.async_write_ha_state() diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 03bb2a26183..2ac322002d5 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from typing import Any from yolink.device import YoLinkDevice +from yolink.outlet_request_builder import OutletRequestBuilder from homeassistant.components.switch import ( SwitchDeviceClass, @@ -19,6 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_COORDINATORS, ATTR_DEVICE_MANIPULATOR, + ATTR_DEVICE_MULTI_OUTLET, ATTR_DEVICE_OUTLET, ATTR_DEVICE_SWITCH, DOMAIN, @@ -32,8 +34,7 @@ class YoLinkSwitchEntityDescription(SwitchEntityDescription): """YoLink SwitchEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - value: Callable[[Any], bool | None] = lambda _: None - state_key: str = "state" + plug_index: int | None = None DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( @@ -41,26 +42,63 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( key="outlet_state", device_class=SwitchDeviceClass.OUTLET, name="State", - value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_OUTLET, ), YoLinkSwitchEntityDescription( key="manipulator_state", name="State", icon="mdi:pipe", - value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MANIPULATOR, ), YoLinkSwitchEntityDescription( key="switch_state", name="State", device_class=SwitchDeviceClass.SWITCH, - value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_SWITCH, ), + YoLinkSwitchEntityDescription( + key="multi_outlet_usb_ports", + name="UsbPorts", + device_class=SwitchDeviceClass.OUTLET, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, + plug_index=0, + ), + YoLinkSwitchEntityDescription( + key="multi_outlet_plug_1", + name="Plug1", + device_class=SwitchDeviceClass.OUTLET, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, + plug_index=1, + ), + YoLinkSwitchEntityDescription( + key="multi_outlet_plug_2", + name="Plug2", + device_class=SwitchDeviceClass.OUTLET, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, + plug_index=2, + ), + YoLinkSwitchEntityDescription( + key="multi_outlet_plug_3", + name="Plug3", + device_class=SwitchDeviceClass.OUTLET, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, + plug_index=3, + ), + YoLinkSwitchEntityDescription( + key="multi_outlet_plug_4", + name="Plug4", + device_class=SwitchDeviceClass.OUTLET, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, + plug_index=4, + ), ) -DEVICE_TYPE = [ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_OUTLET, ATTR_DEVICE_SWITCH] +DEVICE_TYPE = [ + ATTR_DEVICE_MANIPULATOR, + ATTR_DEVICE_MULTI_OUTLET, + ATTR_DEVICE_OUTLET, + ATTR_DEVICE_SWITCH, +] async def async_setup_entry( @@ -108,18 +146,30 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): f"{coordinator.device.device_name} ({self.entity_description.name})" ) + def _get_state( + self, state_value: str | list[str] | None, plug_index: int | None + ) -> bool | None: + """Parse state value.""" + if isinstance(state_value, list) and plug_index is not None: + return state_value[plug_index] == "open" + return state_value == "open" if state_value is not None else None + @callback - def update_entity_state(self, state: dict[str, Any]) -> None: + def update_entity_state(self, state: dict[str, str | list[str]]) -> None: """Update HA Entity State.""" - self._attr_is_on = self.entity_description.value( - state.get(self.entity_description.state_key) + self._attr_is_on = self._get_state( + state.get("state"), self.entity_description.plug_index ) self.async_write_ha_state() async def call_state_change(self, state: str) -> None: """Call setState api to change switch state.""" - await self.call_device_api("setState", {"state": state}) - self._attr_is_on = self.entity_description.value(state) + await self.call_device( + OutletRequestBuilder.set_state_request( + state, self.entity_description.plug_index + ) + ) + self._attr_is_on = self._get_state(state, self.entity_description.plug_index) self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/yolink/translations/bg.json b/homeassistant/components/yolink/translations/bg.json index 5f7e924f493..85093bfd348 100644 --- a/homeassistant/components/yolink/translations/bg.json +++ b/homeassistant/components/yolink/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/yolink/translations/cs.json b/homeassistant/components/yolink/translations/cs.json index 5b7d9c2db8e..67665aec47e 100644 --- a/homeassistant/components/yolink/translations/cs.json +++ b/homeassistant/components/yolink/translations/cs.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "oauth_error": "P\u0159ijata neplatn\u00e1 data tokenu.", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "step": { diff --git a/homeassistant/components/yolink/translations/de.json b/homeassistant/components/yolink/translations/de.json index d9d5c9a6efa..2ab37ac0367 100644 --- a/homeassistant/components/yolink/translations/de.json +++ b/homeassistant/components/yolink/translations/de.json @@ -17,7 +17,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { - "description": "Die yolink-Integration muss dein Konto neu authentifizieren", + "description": "Die yolink Integration muss dein Konto neu authentifizieren", "title": "Integration erneut authentifizieren" } } diff --git a/homeassistant/components/yolink/translations/sk.json b/homeassistant/components/yolink/translations/sk.json new file mode 100644 index 00000000000..d502e9f3f7b --- /dev/null +++ b/homeassistant/components/yolink/translations/sk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "authorize_url_timeout": "\u010casov\u00fd limit generovania autorizovanej adresy URL.", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/sk.json b/homeassistant/components/youless/translations/sk.json index af15f92c2f2..1820d9d8d42 100644 --- a/homeassistant/components/youless/translations/sk.json +++ b/homeassistant/components/youless/translations/sk.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { + "host": "Hostite\u013e", "name": "N\u00e1zov" } } diff --git a/homeassistant/components/zamg/translations/bg.json b/homeassistant/components/zamg/translations/bg.json new file mode 100644 index 00000000000..8e7a1467b4b --- /dev/null +++ b/homeassistant/components/zamg/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}" + }, + "issues": { + "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 ZAMG \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 ZAMG \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 ZAMG \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/ca.json b/homeassistant/components/zamg/translations/ca.json new file mode 100644 index 00000000000..cc116086966 --- /dev/null +++ b/homeassistant/components/zamg/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID de l'estaci\u00f3 (per defecte el de l'estaci\u00f3 m\u00e9s propera)" + }, + "description": "Configura la integraci\u00f3 de ZAMG amb Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de ZAMG mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de ZAMG del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de ZAMG est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/cs.json b/homeassistant/components/zamg/translations/cs.json new file mode 100644 index 00000000000..60fa7fddced --- /dev/null +++ b/homeassistant/components/zamg/translations/cs.json @@ -0,0 +1,11 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/de.json b/homeassistant/components/zamg/translations/de.json index 084d65de978..9aeb4483a81 100644 --- a/homeassistant/components/zamg/translations/de.json +++ b/homeassistant/components/zamg/translations/de.json @@ -1,21 +1,26 @@ { "config": { "abort": { - "already_configured": "Wetterstation ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { - "unknown": "ID der Wetterstation ist unbekannt", "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "{name}", "step": { "user": { "data": { - "station_id": "ID der Wetterstation (nächstgelegene Station as Defaultwert)" + "station_id": "Station-ID (standardm\u00e4\u00dfig n\u00e4chste Station)" }, - "description": "Richte zamg f\u00fcr die Integration mit Home Assistant ein." + "description": "Richte ZAMG f\u00fcr die Integration mit Home Assistant ein." } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von ZAMG mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die ZAMG-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die ZAMG YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/el.json b/homeassistant/components/zamg/translations/el.json new file mode 100644 index 00000000000..9ed237562b4 --- /dev/null +++ b/homeassistant/components/zamg/translations/el.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd (\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c0\u03bb\u03b7\u03c3\u03b9\u03ad\u03c3\u03c4\u03b5\u03c1\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc)" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf ZAMG \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 ZAMG \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 ZAMG YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 ZAMG YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/es.json b/homeassistant/components/zamg/translations/es.json new file mode 100644 index 00000000000..dbd8b9af040 --- /dev/null +++ b/homeassistant/components/zamg/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID de la estaci\u00f3n (por defecto, la estaci\u00f3n m\u00e1s cercana)" + }, + "description": "Configura ZAMG para que se integre con Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de ZAMG mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de ZAMG de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de ZAMG" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/et.json b/homeassistant/components/zamg/translations/et.json new file mode 100644 index 00000000000..b3e0006f63b --- /dev/null +++ b/homeassistant/components/zamg/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "Jaama ID (vaikimisi l\u00e4him jaam)" + }, + "description": "Seadista ZAMG integreerimiseks koduassistendiga." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "ZAMG konfigureerimine YAML-i abil eemaldatakse. \n\n Teie olemasolev YAML-i konfiguratsioon imporditi kasutajaliidesesse automaatselt. \n\n Eemaldage ZAMG YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "ZAMG YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/fr.json b/homeassistant/components/zamg/translations/fr.json new file mode 100644 index 00000000000..f0ce4b1170b --- /dev/null +++ b/homeassistant/components/zamg/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{name}", + "step": { + "user": { + "description": "Configurez ZAMG pour l\u2019int\u00e9grer \u00e0 Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/he.json b/homeassistant/components/zamg/translations/he.json new file mode 100644 index 00000000000..df773347ba2 --- /dev/null +++ b/homeassistant/components/zamg/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/hr.json b/homeassistant/components/zamg/translations/hr.json new file mode 100644 index 00000000000..bc981183e94 --- /dev/null +++ b/homeassistant/components/zamg/translations/hr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "cannot_connect": "Povezivanje nije uspjelo" + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/hu.json b/homeassistant/components/zamg/translations/hu.json new file mode 100644 index 00000000000..339739ea266 --- /dev/null +++ b/homeassistant/components/zamg/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "\u00c1llom\u00e1s azonos\u00edt\u00f3ja (alap\u00e9rtelmez\u00e9s szerint a legk\u00f6zelebbi \u00e1llom\u00e1s)" + }, + "description": "ZAMG \u00e9s Home Assistant integr\u00e1ci\u00f3ja." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A ZAMG YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a ZAMG YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A ZAMG YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/id.json b/homeassistant/components/zamg/translations/id.json new file mode 100644 index 00000000000..4330ae8c3de --- /dev/null +++ b/homeassistant/components/zamg/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID Stasiun (Default ke stasiun terdekat)" + }, + "description": "Siapkan ZAMG untuk diintegrasikan dengan Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi ZAMG lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi ZAMG dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML ZAMG dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/it.json b/homeassistant/components/zamg/translations/it.json new file mode 100644 index 00000000000..8ec3f4a93bc --- /dev/null +++ b/homeassistant/components/zamg/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID stazione (predefinito alla stazione pi\u00f9 vicina)" + }, + "description": "Configura ZAMG per l'integrazione con Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di ZAMG tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovere la configurazione YAML di ZAMG dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di ZAMG \u00e8 stata rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/nl.json b/homeassistant/components/zamg/translations/nl.json new file mode 100644 index 00000000000..2bdc82453e8 --- /dev/null +++ b/homeassistant/components/zamg/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/no.json b/homeassistant/components/zamg/translations/no.json new file mode 100644 index 00000000000..265fff5c70d --- /dev/null +++ b/homeassistant/components/zamg/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "Stasjons-ID (standard til n\u00e6rmeste stasjon)" + }, + "description": "Konfigurer ZAMG for \u00e5 integrere med Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av ZAMG med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern ZAMG YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "ZAMG YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/pl.json b/homeassistant/components/zamg/translations/pl.json new file mode 100644 index 00000000000..8837a14ecd6 --- /dev/null +++ b/homeassistant/components/zamg/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "Identyfikator stacji (domy\u015blnie najbli\u017csza stacja)" + }, + "description": "Skonfiguruj ZAMG, aby zintegrowa\u0107 go z Home Assistantem." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja ZAMG przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla ZAMG zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/pt-BR.json b/homeassistant/components/zamg/translations/pt-BR.json new file mode 100644 index 00000000000..e35fbd0e49e --- /dev/null +++ b/homeassistant/components/zamg/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID da esta\u00e7\u00e3o (Padr\u00e3o para a esta\u00e7\u00e3o mais pr\u00f3xima)" + }, + "description": "Configure o ZAMG para integrar com o Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do ZAMG usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o ZAMG YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de ZAMG est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/ru.json b/homeassistant/components/zamg/translations/ru.json new file mode 100644 index 00000000000..59439e4789b --- /dev/null +++ b/homeassistant/components/zamg/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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\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." + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID \u0441\u0442\u0430\u043d\u0446\u0438\u0438 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0430\u044f \u0441\u0442\u0430\u043d\u0446\u0438\u044f)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 ZAMG." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ZAMG \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ZAMG \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/sk.json b/homeassistant/components/zamg/translations/sk.json new file mode 100644 index 00000000000..dddfc872188 --- /dev/null +++ b/homeassistant/components/zamg/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID stanice (predvolen\u00e9 nastavenie na najbli\u017e\u0161iu stanicu)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/tr.json b/homeassistant/components/zamg/translations/tr.json new file mode 100644 index 00000000000..3ebc2a2788b --- /dev/null +++ b/homeassistant/components/zamg/translations/tr.json @@ -0,0 +1,26 @@ +{ + "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": "{name}", + "step": { + "user": { + "data": { + "station_id": "\u0130stasyon Kimli\u011fi (Varsay\u0131lan olarak en yak\u0131n istasyon)" + }, + "description": "ZAMG'yi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "ZAMG'nin YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n ZAMG YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "ZAMG YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/zh-Hant.json b/homeassistant/components/zamg/translations/zh-Hant.json new file mode 100644 index 00000000000..3e8bccdb4f5 --- /dev/null +++ b/homeassistant/components/zamg/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "\u76e3\u6e2c\u7ad9 ID\uff08\u9810\u8a2d\u70ba\u6700\u8fd1\u7684\u76e3\u6e2c\u7ad9\uff09" + }, + "description": "\u8a2d\u5b9a ZAMG \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 ZAMG \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 ZAMG YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "ZAMG YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index b9d8ba67bbf..39c9d77f071 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -9,10 +9,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -83,10 +83,10 @@ class ZamgWeather(CoordinatorEntity, WeatherEntity): name=coordinator.name, ) # set units of ZAMG API - self._attr_native_temperature_unit = TEMP_CELSIUS - self._attr_native_pressure_unit = PRESSURE_HPA - self._attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND - self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_native_pressure_unit = UnitOfPressure.HPA + self._attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS @property def condition(self) -> str | None: diff --git a/homeassistant/components/zerproc/translations/he.json b/homeassistant/components/zerproc/translations/he.json index 459053197a6..c6da7f61442 100644 --- a/homeassistant/components/zerproc/translations/he.json +++ b/homeassistant/components/zerproc/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8. \u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d7\u05d9\u05d3\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/zerproc/translations/sk.json b/homeassistant/components/zerproc/translations/sk.json new file mode 100644 index 00000000000..d4bb209c34c --- /dev/null +++ b/homeassistant/components/zerproc/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 0a7d43120f7..70b9dfd9b46 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -35,7 +35,6 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, - ZHA_DEVICES_LOADED_EVENT, RadioType, ) from .core.discovery import GROUP_PROBE @@ -76,7 +75,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up ZHA from config.""" - hass.data[DATA_ZHA] = {ZHA_DEVICES_LOADED_EVENT: asyncio.Event()} + hass.data[DATA_ZHA] = {} if DOMAIN in config: conf = config[DOMAIN] @@ -110,7 +109,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() - hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].set() device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -143,7 +141,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].clear() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index abddfda3358..1250e3c92a7 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -194,3 +194,12 @@ class ReplaceFilter(BinarySensor, id_suffix="replace_filter"): SENSOR_ATTR = "replace_filter" _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM + + +@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederErrorDetected(BinarySensor, id_suffix="error_detected"): + """ZHA aqara pet feeder error detected binary sensor.""" + + SENSOR_ATTR = "error_detected" + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM + _attr_name: str = "Error detected" diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 41f3846e97f..e41e7a81e12 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -177,3 +177,12 @@ class NoPresenceStatusResetButton( _attribute_value = 1 _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG + + +@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederFeedButton(ZHAAttributeButton, id_suffix="feeding"): + """Defines a feed button for the aqara c1 pet feeder.""" + + _attribute_name = "feeding" + _attr_name = "Feed" + _attribute_value = 1 diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 155a254217e..4de07bf0d74 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -277,7 +277,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return self._presets @property - def supported_features(self) -> int: + def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = self._supported_flags if HVACMode.HEAT_COOL in self.hvac_modes: diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 85f03b9f1f5..df5fa047c99 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,44 +1,32 @@ """Config flow for ZHA.""" from __future__ import annotations -import asyncio import collections -import contextlib -import copy import json -import logging -import os from typing import Any import serial.tools.list_ports import voluptuous as vol -from zigpy.application import ControllerApplication import zigpy.backups from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH -from zigpy.exceptions import NetworkNotFormed from homeassistant import config_entries from homeassistant.components import onboarding, usb, zeroconf from homeassistant.components.file_upload import process_uploaded_file from homeassistant.const import CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowHandler, FlowResult from homeassistant.helpers.selector import FileSelector, FileSelectorConfig from homeassistant.util import dt from .core.const import ( CONF_BAUDRATE, - CONF_DATABASE, CONF_FLOWCONTROL, CONF_RADIO_TYPE, - CONF_ZIGPY, - DATA_ZHA, - DATA_ZHA_CONFIG, - DEFAULT_DATABASE_NAME, DOMAIN, - EZSP_OVERWRITE_EUI64, RadioType, ) +from .radio_manager import HARDWARE_DISCOVERY_SCHEMA, ZhaRadioManager CONF_MANUAL_PATH = "Enter Manually" SUPPORTED_PORT_SETTINGS = ( @@ -47,16 +35,6 @@ SUPPORTED_PORT_SETTINGS = ( ) DECONZ_DOMAIN = "deconz" -# Only the common radio types will be autoprobed, ordered by new device popularity. -# XBee takes too long to probe since it scans through all possible bauds and likely has -# very few users to begin with. -AUTOPROBE_RADIOS = ( - RadioType.ezsp, - RadioType.znp, - RadioType.deconz, - RadioType.zigate, -) - FORMATION_STRATEGY = "formation_strategy" FORMATION_FORM_NEW_NETWORK = "form_new_network" FORMATION_REUSE_SETTINGS = "reuse_settings" @@ -74,10 +52,6 @@ UPLOADED_BACKUP_FILE = "uploaded_backup_file" DEFAULT_ZHA_ZEROCONF_PORT = 6638 ESPHOME_API_PORT = 6053 -CONNECT_DELAY_S = 1.0 - -_LOGGER = logging.getLogger(__name__) - def _format_backup_choice( backup: zigpy.backups.NetworkBackup, *, pan_ids: bool = True @@ -96,133 +70,47 @@ def _format_backup_choice( return f"{dt.as_local(backup.backup_time).strftime('%c')} ({identifier})" -def _allow_overwrite_ezsp_ieee( - backup: zigpy.backups.NetworkBackup, -) -> zigpy.backups.NetworkBackup: - """Return a new backup with the flag to allow overwriting the EZSP EUI64.""" - new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) - new_stack_specific.setdefault("ezsp", {})[EZSP_OVERWRITE_EUI64] = True - - return backup.replace( - network_info=backup.network_info.replace(stack_specific=new_stack_specific) - ) - - -def _prevent_overwrite_ezsp_ieee( - backup: zigpy.backups.NetworkBackup, -) -> zigpy.backups.NetworkBackup: - """Return a new backup without the flag to allow overwriting the EZSP EUI64.""" - if "ezsp" not in backup.network_info.stack_specific: - return backup - - new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) - new_stack_specific.setdefault("ezsp", {}).pop(EZSP_OVERWRITE_EUI64, None) - - return backup.replace( - network_info=backup.network_info.replace(stack_specific=new_stack_specific) - ) - - class BaseZhaFlow(FlowHandler): """Mixin for common ZHA flow steps and forms.""" + _hass: HomeAssistant + def __init__(self) -> None: """Initialize flow instance.""" super().__init__() - self._device_path: str | None = None - self._device_settings: dict[str, Any] | None = None - self._radio_type: RadioType | None = None + self._hass = None # type: ignore[assignment] + self._radio_mgr = ZhaRadioManager() self._title: str | None = None - self._current_settings: zigpy.backups.NetworkBackup | None = None - self._backups: list[zigpy.backups.NetworkBackup] = [] - self._chosen_backup: zigpy.backups.NetworkBackup | None = None - @contextlib.asynccontextmanager - async def _connect_zigpy_app(self) -> ControllerApplication: - """Connect to the radio with the current config and then clean up.""" - assert self._radio_type is not None + @property + def hass(self): + """Return hass.""" + return self._hass - config = self.hass.data.get(DATA_ZHA, {}).get(DATA_ZHA_CONFIG, {}) - app_config = config.get(CONF_ZIGPY, {}).copy() + @hass.setter + def hass(self, hass): + """Set hass.""" + self._hass = hass + self._radio_mgr.hass = hass - database_path = config.get( - CONF_DATABASE, - self.hass.config.path(DEFAULT_DATABASE_NAME), - ) - - # Don't create `zigbee.db` if it doesn't already exist - if not await self.hass.async_add_executor_job(os.path.exists, database_path): - database_path = None - - app_config[CONF_DATABASE] = database_path - app_config[CONF_DEVICE] = self._device_settings - app_config = self._radio_type.controller.SCHEMA(app_config) - - app = await self._radio_type.controller.new( - app_config, auto_form=False, start_radio=False - ) - - try: - await app.connect() - yield app - finally: - await app.disconnect() - await asyncio.sleep(CONNECT_DELAY_S) - - async def _restore_backup( - self, backup: zigpy.backups.NetworkBackup, **kwargs: Any - ) -> None: - """Restore the provided network backup, passing through kwargs.""" - if self._current_settings is not None and self._current_settings.supersedes( - self._chosen_backup - ): - return - - async with self._connect_zigpy_app() as app: - await app.backups.restore_backup(backup, **kwargs) - - async def _detect_radio_type(self) -> bool: - """Probe all radio types on the current port.""" - for radio in AUTOPROBE_RADIOS: - _LOGGER.debug("Attempting to probe radio type %s", radio) - - dev_config = radio.controller.SCHEMA_DEVICE( - {CONF_DEVICE_PATH: self._device_path} - ) - probe_result = await radio.controller.probe(dev_config) - - if not probe_result: - continue - - # Radio library probing can succeed and return new device settings - if isinstance(probe_result, dict): - dev_config = probe_result - - self._radio_type = radio - self._device_settings = dev_config - - return True - - return False - - async def _async_create_radio_entity(self) -> FlowResult: - """Create a config entity with the current flow state.""" + async def _async_create_radio_entry(self) -> FlowResult: + """Create a config entry with the current flow state.""" assert self._title is not None - assert self._radio_type is not None - assert self._device_path is not None - assert self._device_settings is not None + assert self._radio_mgr.radio_type is not None + assert self._radio_mgr.device_path is not None + assert self._radio_mgr.device_settings is not None - device_settings = self._device_settings.copy() + device_settings = self._radio_mgr.device_settings.copy() device_settings[CONF_DEVICE_PATH] = await self.hass.async_add_executor_job( - usb.get_serial_by_id, self._device_path + usb.get_serial_by_id, self._radio_mgr.device_path ) return self.async_create_entry( title=self._title, data={ CONF_DEVICE: device_settings, - CONF_RADIO_TYPE: self._radio_type.name, + CONF_RADIO_TYPE: self._radio_mgr.radio_type.name, }, ) @@ -249,9 +137,9 @@ class BaseZhaFlow(FlowHandler): return await self.async_step_manual_pick_radio_type() port = ports[list_of_ports.index(user_selection)] - self._device_path = port.device + self._radio_mgr.device_path = port.device - if not await self._detect_radio_type(): + if not await self._radio_mgr.detect_radio_type(): # Did not autodetect anything, proceed to manual selection return await self.async_step_manual_pick_radio_type() @@ -267,9 +155,9 @@ class BaseZhaFlow(FlowHandler): # Pre-select the currently configured port default_port = vol.UNDEFINED - if self._device_path is not None: + if self._radio_mgr.device_path is not None: for description, port in zip(list_of_ports, ports): - if port.device == self._device_path: + if port.device == self._radio_mgr.device_path: default_port = description break else: @@ -289,14 +177,16 @@ class BaseZhaFlow(FlowHandler): ) -> FlowResult: """Manually select the radio type.""" if user_input is not None: - self._radio_type = RadioType.get_by_description(user_input[CONF_RADIO_TYPE]) + self._radio_mgr.radio_type = RadioType.get_by_description( + user_input[CONF_RADIO_TYPE] + ) return await self.async_step_manual_port_config() # Pre-select the current radio type default = vol.UNDEFINED - if self._radio_type is not None: - default = self._radio_type.description + if self._radio_mgr.radio_type is not None: + default = self._radio_mgr.radio_type.description schema = { vol.Required(CONF_RADIO_TYPE, default=default): vol.In(RadioType.list()) @@ -311,35 +201,43 @@ class BaseZhaFlow(FlowHandler): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Enter port settings specific for this type of radio.""" - assert self._radio_type is not None + assert self._radio_mgr.radio_type is not None errors = {} if user_input is not None: self._title = user_input[CONF_DEVICE_PATH] - self._device_path = user_input[CONF_DEVICE_PATH] - self._device_settings = user_input.copy() + self._radio_mgr.device_path = user_input[CONF_DEVICE_PATH] + self._radio_mgr.device_settings = user_input.copy() - if await self._radio_type.controller.probe(user_input): + if await self._radio_mgr.radio_type.controller.probe(user_input): return await self.async_step_choose_formation_strategy() errors["base"] = "cannot_connect" schema = { vol.Required( - CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED + CONF_DEVICE_PATH, default=self._radio_mgr.device_path or vol.UNDEFINED ): str } source = self.context.get("source") - for param, value in self._radio_type.controller.SCHEMA_DEVICE.schema.items(): + for ( + param, + value, + ) in self._radio_mgr.radio_type.controller.SCHEMA_DEVICE.schema.items(): if param not in SUPPORTED_PORT_SETTINGS: continue if source == config_entries.SOURCE_ZEROCONF and param == CONF_BAUDRATE: value = 115200 param = vol.Required(CONF_BAUDRATE, default=value) - elif self._device_settings is not None and param in self._device_settings: - param = vol.Required(str(param), default=self._device_settings[param]) + elif ( + self._radio_mgr.device_settings is not None + and param in self._radio_mgr.device_settings + ): + param = vol.Required( + str(param), default=self._radio_mgr.device_settings[param] + ) schema[param] = value @@ -349,43 +247,26 @@ class BaseZhaFlow(FlowHandler): errors=errors, ) - async def _async_load_network_settings(self) -> None: - """Connect to the radio and load its current network settings.""" - async with self._connect_zigpy_app() as app: - # Check if the stick has any settings and load them - try: - await app.load_network_info() - except NetworkNotFormed: - pass - else: - self._current_settings = zigpy.backups.NetworkBackup( - network_info=app.state.network_info, - node_info=app.state.node_info, - ) - - # The list of backups will always exist - self._backups = app.backups.backups.copy() - async def async_step_choose_formation_strategy( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Choose how to deal with the current radio's settings.""" - await self._async_load_network_settings() + await self._radio_mgr.async_load_network_settings() strategies = [] # Check if we have any automatic backups *and* if the backups differ from # the current radio settings, if they exist (since restoring would be redundant) - if self._backups and ( - self._current_settings is None + if self._radio_mgr.backups and ( + self._radio_mgr.current_settings is None or any( - not backup.is_compatible_with(self._current_settings) - for backup in self._backups + not backup.is_compatible_with(self._radio_mgr.current_settings) + for backup in self._radio_mgr.backups ) ): strategies.append(CHOOSE_AUTOMATIC_BACKUP) - if self._current_settings is not None: + if self._radio_mgr.current_settings is not None: strategies.append(FORMATION_REUSE_SETTINGS) strategies.append(FORMATION_UPLOAD_MANUAL_BACKUP) @@ -400,16 +281,14 @@ class BaseZhaFlow(FlowHandler): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Reuse the existing network settings on the stick.""" - return await self._async_create_radio_entity() + return await self._async_create_radio_entry() async def async_step_form_new_network( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Form a brand new network.""" - async with self._connect_zigpy_app() as app: - await app.form_network() - - return await self._async_create_radio_entity() + await self._radio_mgr.async_form_network() + return await self._async_create_radio_entry() def _parse_uploaded_backup( self, uploaded_file_id: str @@ -428,7 +307,7 @@ class BaseZhaFlow(FlowHandler): if user_input is not None: try: - self._chosen_backup = await self.hass.async_add_executor_job( + self._radio_mgr.chosen_backup = await self.hass.async_add_executor_job( self._parse_uploaded_backup, user_input[UPLOADED_BACKUP_FILE] ) except ValueError: @@ -455,23 +334,24 @@ class BaseZhaFlow(FlowHandler): if self.show_advanced_options: # Always show the PAN IDs when in advanced mode choices = [ - _format_backup_choice(backup, pan_ids=True) for backup in self._backups + _format_backup_choice(backup, pan_ids=True) + for backup in self._radio_mgr.backups ] else: # Only show the PAN IDs for multiple backups taken on the same day num_backups_on_date = collections.Counter( - backup.backup_time.date() for backup in self._backups + backup.backup_time.date() for backup in self._radio_mgr.backups ) choices = [ _format_backup_choice( backup, pan_ids=(num_backups_on_date[backup.backup_time.date()] > 1) ) - for backup in self._backups + for backup in self._radio_mgr.backups ] if user_input is not None: index = choices.index(user_input[CHOOSE_AUTOMATIC_BACKUP]) - self._chosen_backup = self._backups[index] + self._radio_mgr.chosen_backup = self._radio_mgr.backups[index] return await self.async_step_maybe_confirm_ezsp_restore() @@ -490,46 +370,15 @@ class BaseZhaFlow(FlowHandler): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm restore for EZSP radios that require permanent IEEE writes.""" - assert self._chosen_backup is not None - - if self._radio_type != RadioType.ezsp: - await self._restore_backup(self._chosen_backup) - return await self._async_create_radio_entity() - - # We have no way to partially load network settings if no network is formed - if self._current_settings is None: - # Since we are going to be restoring the backup anyways, write it to the - # radio without overwriting the IEEE but don't take a backup with these - # temporary settings - temp_backup = _prevent_overwrite_ezsp_ieee(self._chosen_backup) - await self._restore_backup(temp_backup, create_new=False) - await self._async_load_network_settings() - - assert self._current_settings is not None - - if ( - self._current_settings.node_info.ieee == self._chosen_backup.node_info.ieee - or not self._current_settings.network_info.metadata["ezsp"][ - "can_write_custom_eui64" - ] - ): - # No point in prompting the user if the backup doesn't have a new IEEE - # address or if there is no way to overwrite the IEEE address a second time - await self._restore_backup(self._chosen_backup) - - return await self._async_create_radio_entity() + call_step_2 = await self._radio_mgr.async_restore_backup_step_1() + if not call_step_2: + return await self._async_create_radio_entry() if user_input is not None: - backup = self._chosen_backup - - if user_input[OVERWRITE_COORDINATOR_IEEE]: - backup = _allow_overwrite_ezsp_ieee(backup) - - # If the user declined to overwrite the IEEE *and* we wrote the backup to - # their empty radio above, restoring it again would be redundant. - await self._restore_backup(backup) - - return await self._async_create_radio_entity() + await self._radio_mgr.async_restore_backup_step_2( + user_input[OVERWRITE_COORDINATOR_IEEE] + ) + return await self._async_create_radio_entry() return self.async_show_form( step_id="maybe_confirm_ezsp_restore", @@ -544,6 +393,24 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN VERSION = 3 + async def _set_unique_id_or_update_path( + self, unique_id: str, device_path: str + ) -> None: + """Set the flow's unique ID and update the device path if it isn't unique.""" + current_entry = await self.async_set_unique_id(unique_id) + + if not current_entry: + return + + self._abort_if_unique_id_configured( + updates={ + CONF_DEVICE: { + **current_entry.data.get(CONF_DEVICE, {}), + CONF_DEVICE_PATH: device_path, + }, + } + ) + @staticmethod @callback def async_get_options_flow( @@ -575,13 +442,16 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN # config flow logic that interacts with hardware! if user_input is not None or not onboarding.async_is_onboarded(self.hass): # Probe the radio type if we don't have one yet - if self._radio_type is None and not await self._detect_radio_type(): + if ( + self._radio_mgr.radio_type is None + and not await self._radio_mgr.detect_radio_type() + ): # This path probably will not happen now that we have # more precise USB matching unless there is a problem # with the device return self.async_abort(reason="usb_probe_failed") - if self._device_settings is None: + if self._radio_mgr.device_settings is None: return await self.async_step_manual_port_config() return await self.async_step_choose_formation_strategy() @@ -600,16 +470,11 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN manufacturer = discovery_info.manufacturer description = discovery_info.description dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, device) - unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" - if current_entry := await self.async_set_unique_id(unique_id): - self._abort_if_unique_id_configured( - updates={ - CONF_DEVICE: { - **current_entry.data.get(CONF_DEVICE, {}), - CONF_DEVICE_PATH: dev_path, - }, - } - ) + + await self._set_unique_id_or_update_path( + unique_id=f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}", + device_path=dev_path, + ) # If they already have a discovery for deconz we ignore the usb discovery as # they probably want to use it there instead @@ -619,7 +484,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN if entry.source != config_entries.SOURCE_IGNORE: return self.async_abort(reason="not_zha_device") - self._device_path = dev_path + self._radio_mgr.device_path = dev_path self._title = description or usb.human_readable_device_name( dev_path, serial_number, @@ -645,28 +510,25 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN port = DEFAULT_ZHA_ZEROCONF_PORT if "radio_type" in discovery_info.properties: - self._radio_type = RadioType[discovery_info.properties["radio_type"]] + self._radio_mgr.radio_type = self._radio_mgr.parse_radio_type( + discovery_info.properties["radio_type"] + ) elif "efr32" in local_name: - self._radio_type = RadioType.ezsp + self._radio_mgr.radio_type = RadioType.ezsp else: - self._radio_type = RadioType.znp + self._radio_mgr.radio_type = RadioType.znp node_name = local_name[: -len(".local")] device_path = f"socket://{discovery_info.host}:{port}" - if current_entry := await self.async_set_unique_id(node_name): - self._abort_if_unique_id_configured( - updates={ - CONF_DEVICE: { - **current_entry.data.get(CONF_DEVICE, {}), - CONF_DEVICE_PATH: device_path, - }, - } - ) + await self._set_unique_id_or_update_path( + unique_id=node_name, + device_path=device_path, + ) self.context["title_placeholders"] = {CONF_NAME: node_name} self._title = device_path - self._device_path = device_path + self._radio_mgr.device_path = device_path return await self.async_step_confirm() @@ -674,34 +536,31 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN self, data: dict[str, Any] | None = None ) -> FlowResult: """Handle hardware flow.""" - if not data: - return self.async_abort(reason="invalid_hardware_data") - if data.get("radio_type") != "efr32": - return self.async_abort(reason="invalid_hardware_data") - - self._radio_type = RadioType.ezsp - - schema = { - vol.Required( - CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED - ): str - } - - radio_schema = self._radio_type.controller.SCHEMA_DEVICE.schema - assert not isinstance(radio_schema, vol.Schema) - - for param, value in radio_schema.items(): - if param in SUPPORTED_PORT_SETTINGS: - schema[param] = value - try: - device_settings = vol.Schema(schema)(data.get("port")) + discovery_data = HARDWARE_DISCOVERY_SCHEMA(data) except vol.Invalid: return self.async_abort(reason="invalid_hardware_data") - self._title = data.get("name", data["port"]["path"]) - self._device_path = device_settings[CONF_DEVICE_PATH] - self._device_settings = device_settings + name = discovery_data["name"] + radio_type = self._radio_mgr.parse_radio_type(discovery_data["radio_type"]) + + try: + device_settings = radio_type.controller.SCHEMA_DEVICE( + discovery_data["port"] + ) + except vol.Invalid: + return self.async_abort(reason="invalid_hardware_data") + + await self._set_unique_id_or_update_path( + unique_id=f"{name}_{radio_type.name}_{device_settings[CONF_DEVICE_PATH]}", + device_path=device_settings[CONF_DEVICE_PATH], + ) + + self._title = name + self._radio_mgr.radio_type = radio_type + self._radio_mgr.device_path = device_settings[CONF_DEVICE_PATH] + self._radio_mgr.device_settings = device_settings + self.context["title_placeholders"] = {CONF_NAME: name} return await self.async_step_confirm() @@ -714,9 +573,9 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): super().__init__() self.config_entry = config_entry - self._device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - self._device_settings = config_entry.data[CONF_DEVICE] - self._radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]] + self._radio_mgr.device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + self._radio_mgr.device_settings = config_entry.data[CONF_DEVICE] + self._radio_mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]] self._title = config_entry.title async def async_step_init( @@ -759,9 +618,7 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): """Confirm the user wants to reset their current radio.""" if user_input is not None: - # Reset the current adapter - async with self._connect_zigpy_app() as app: - await app.reset_network_info() + await self._radio_mgr.async_reset_adapter() return await self.async_step_instruct_unplug() @@ -778,11 +635,11 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): return self.async_show_form(step_id="instruct_unplug") - async def _async_create_radio_entity(self): + async def _async_create_radio_entry(self): """Re-implementation of the base flow's final step to update the config.""" - device_settings = self._device_settings.copy() + device_settings = self._radio_mgr.device_settings.copy() device_settings[CONF_DEVICE_PATH] = await self.hass.async_add_executor_job( - usb.get_serial_by_id, self._device_path + usb.get_serial_by_id, self._radio_mgr.device_path ) # Avoid creating both `.options` and `.data` by directly writing `data` here @@ -790,7 +647,7 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): entry=self.config_entry, data={ CONF_DEVICE: device_settings, - CONF_RADIO_TYPE: self._radio_type.name, + CONF_RADIO_TYPE: self._radio_mgr.radio_type.name, }, options=self.config_entry.options, ) diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py index a416ff2eebe..755eac3c4ce 100644 --- a/homeassistant/components/zha/core/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -1,5 +1,6 @@ """Core module for Zigbee Home Automation.""" -# flake8: noqa from .device import ZHADevice from .gateway import ZHAGateway + +__all__ = ["ZHADevice", "ZHAGateway"] diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index c028a6021da..47d0cafb01c 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -161,6 +161,12 @@ class BasicChannel(ZigbeeChannel): self.ZCL_INIT_ATTRS.copy() ) self.ZCL_INIT_ATTRS["trigger_indicator"] = True + elif ( + self.cluster.endpoint.manufacturer == "TexasInstruments" + and self.cluster.endpoint.model == "ti.router" + ): + self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() + self.ZCL_INIT_ATTRS["transmit_power"] = True @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) @@ -352,7 +358,10 @@ class OnOffChannel(ZigbeeChannel): self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name self.ZCL_INIT_ATTRS.copy() ) + self.ZCL_INIT_ATTRS["backlight_mode"] = True self.ZCL_INIT_ATTRS["power_on_state"] = True + if self.cluster.endpoint.model == "TS011F": + self.ZCL_INIT_ATTRS["child_lock"] = True @property def on_off(self) -> bool | None: diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index c4baccf4ae6..427579cfb59 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -110,6 +110,11 @@ class OppleRemote(ZigbeeChannel): "motion_sensitivity": True, "trigger_indicator": True, } + elif self.cluster.endpoint.model == "lumi.motion.agl04": + self.ZCL_INIT_ATTRS = { + "detection_interval": True, + "motion_sensitivity": True, + } elif self.cluster.endpoint.model == "lumi.motion.ac01": self.ZCL_INIT_ATTRS = { "presence": True, @@ -121,10 +126,21 @@ class OppleRemote(ZigbeeChannel): self.ZCL_INIT_ATTRS = { "power_outage_memory": True, } + elif self.cluster.endpoint.model == "aqara.feeder.acn001": + self.ZCL_INIT_ATTRS = { + "portions_dispensed": True, + "weight_dispensed": True, + "error_detected": True, + "disable_led_indicator": True, + "child_lock": True, + "feeding_mode": True, + "serving_size": True, + "portion_weight": True, + } async def async_initialize_channel_specific(self, from_cache: bool) -> None: """Initialize channel specific.""" - if self.cluster.endpoint.model == "lumi.motion.ac02": + if self.cluster.endpoint.model in ("lumi.motion.ac02", "lumi.motion.agl04"): interval = self.cluster.get("detection_interval", self.cluster.get(0x0102)) if interval is not None: self.debug("Loaded detection interval at startup: %s", interval) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index b9871a1f2ab..c6958abc742 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -49,6 +49,7 @@ ATTR_POWER_SOURCE = "power_source" ATTR_PROFILE_ID = "profile_id" ATTR_QUIRK_APPLIED = "quirk_applied" ATTR_QUIRK_CLASS = "quirk_class" +ATTR_ROUTES = "routes" ATTR_RSSI = "rssi" ATTR_SIGNATURE = "signature" ATTR_TYPE = "type" @@ -395,7 +396,6 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" -ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event" class Strobe(t.enum8): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 5eb436cbe53..dfed1ce0ebe 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -57,6 +57,7 @@ from .const import ( ATTR_POWER_SOURCE, ATTR_QUIRK_APPLIED, ATTR_QUIRK_CLASS, + ATTR_ROUTES, ATTR_RSSI, ATTR_SIGNATURE, ATTR_VALUE, @@ -523,20 +524,32 @@ class ZHADevice(LogMixin): for entity_ref in self.gateway.device_registry[self.ieee] ] - # Return the neighbor information + topology = self.gateway.application_controller.topology device_info[ATTR_NEIGHBORS] = [ { - "device_type": neighbor.neighbor.device_type.name, - "rx_on_when_idle": neighbor.neighbor.rx_on_when_idle.name, - "relationship": neighbor.neighbor.relationship.name, - "extended_pan_id": str(neighbor.neighbor.extended_pan_id), - "ieee": str(neighbor.neighbor.ieee), - "nwk": str(neighbor.neighbor.nwk), - "permit_joining": neighbor.neighbor.permit_joining.name, - "depth": str(neighbor.neighbor.depth), - "lqi": str(neighbor.neighbor.lqi), + "device_type": neighbor.device_type.name, + "rx_on_when_idle": neighbor.rx_on_when_idle.name, + "relationship": neighbor.relationship.name, + "extended_pan_id": str(neighbor.extended_pan_id), + "ieee": str(neighbor.ieee), + "nwk": str(neighbor.nwk), + "permit_joining": neighbor.permit_joining.name, + "depth": str(neighbor.depth), + "lqi": str(neighbor.lqi), } - for neighbor in self._zigpy_device.neighbors + for neighbor in topology.neighbors[self.ieee] + ] + + device_info[ATTR_ROUTES] = [ + { + "dest_nwk": str(route.DstNWK), + "route_status": str(route.RouteStatus.name), + "memory_constrained": bool(route.MemoryConstrained), + "many_to_one": bool(route.ManyToOne), + "route_record_required": bool(route.RouteRecordRequired), + "next_hop": str(route.NextHop), + } + for route in topology.routes[self.ieee] ] # Return endpoint device type Names diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 8ecc85a4e56..1a636ce65a2 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -4,8 +4,7 @@ from __future__ import annotations import functools import time -from homeassistant.components.device_tracker import SourceType -from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 94b94b89e40..6f78aa6f858 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -14,7 +14,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN as ZHA_DOMAIN -from .core.const import DATA_ZHA, ZHA_DEVICES_LOADED_EVENT, ZHA_EVENT +from .core.const import ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" @@ -32,18 +32,16 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) - if ZHA_DOMAIN in hass.config.components: - await hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].wait() - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - try: - zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError, IntegrationError) as err: - raise InvalidDeviceAutomationConfig from err - if ( - zha_device.device_automation_triggers is None - or trigger not in zha_device.device_automation_triggers - ): - raise InvalidDeviceAutomationConfig + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + try: + zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) + except (KeyError, AttributeError, IntegrationError) as err: + raise InvalidDeviceAutomationConfig from err + if ( + zha_device.device_automation_triggers is None + or trigger not in zha_device.device_automation_triggers + ): + raise InvalidDeviceAutomationConfig return config diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 2953ab3b99a..b4c760b27ec 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -18,6 +18,7 @@ from zigpy.zcl.foundation import Status from homeassistant.components import light from homeassistant.components.light import ( ColorMode, + LightEntityFeature, brightness_supported, filter_supported_color_modes, ) @@ -1078,7 +1079,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): set[str], set().union(*all_supported_color_modes) ) - self._attr_supported_features = 0 + self._attr_supported_features = LightEntityFeature(0) for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature # we find. diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d2b19fe8893..50a30142bc5 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,15 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.4", + "bellows==0.34.5", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.87", - "zigpy-deconz==0.19.1", - "zigpy==0.51.6", + "zha-quirks==0.0.88", + "zigpy-deconz==0.19.2", + "zigpy==0.52.3", "zigpy-xbee==0.16.2", "zigpy-zigate==0.10.3", - "zigpy-znp==0.9.1" + "zigpy-znp==0.9.2" ], "usb": [ { diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 1776cabf125..41e90899894 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -8,9 +8,9 @@ from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.foundation import Status -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import Platform, UnitOfMass from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory @@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import ( CHANNEL_ANALOG_OUTPUT, + CHANNEL_BASIC, CHANNEL_COLOR, CHANNEL_INOVELLI, CHANNEL_LEVEL, @@ -447,7 +448,9 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): _LOGGER.debug("read value=%s", value) -@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"}) +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="opple_cluster", models={"lumi.motion.ac02", "lumi.motion.agl04"} +) class AqaraMotionDetectionInterval( ZHANumberConfigurationEntity, id_suffix="detection_interval" ): @@ -585,6 +588,20 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time") _attr_name = "Filter life time" +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_BASIC, + manufacturers={"TexasInstruments"}, + models={"ti.router"}, +) +class TiRouterTransmitPower(ZHANumberConfigurationEntity, id_suffix="transmit_power"): + """Representation of a ZHA TI transmit power configuration entity.""" + + _attr_native_min_value: float = -20 + _attr_native_max_value: float = 20 + _zcl_attribute: str = "transmit_power" + _attr_name = "Transmit power" + + @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) class InovelliRemoteDimmingUpSpeed( ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_remote" @@ -819,3 +836,32 @@ class InovelliDefaultAllLEDOffIntensity( _attr_native_max_value: float = 100 _zcl_attribute: str = "led_intensity_when_off" _attr_name: str = "Default all LED off intensity" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederServingSize(ZHANumberConfigurationEntity, id_suffix="serving_size"): + """Aqara pet feeder serving size configuration entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value: float = 1 + _attr_native_max_value: float = 10 + _zcl_attribute: str = "serving_size" + _attr_name: str = "Serving to dispense" + _attr_mode: NumberMode = NumberMode.BOX + _attr_icon: str = "mdi:counter" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederPortionWeight( + ZHANumberConfigurationEntity, id_suffix="portion_weight" +): + """Aqara pet feeder portion weight configuration entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value: float = 1 + _attr_native_max_value: float = 100 + _zcl_attribute: str = "portion_weight" + _attr_name: str = "Portion weight" + _attr_mode: NumberMode = NumberMode.BOX + _attr_native_unit_of_measurement: str = UnitOfMass.GRAMS + _attr_icon: str = "mdi:weight-gram" diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py new file mode 100644 index 00000000000..2a914dee1d7 --- /dev/null +++ b/homeassistant/components/zha/radio_manager.py @@ -0,0 +1,388 @@ +"""Config flow for ZHA.""" +from __future__ import annotations + +import asyncio +import contextlib +import copy +import logging +import os +from typing import Any + +import voluptuous as vol +from zigpy.application import ControllerApplication +import zigpy.backups +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.exceptions import NetworkNotFormed + +from homeassistant import config_entries +from homeassistant.components import usb +from homeassistant.core import HomeAssistant + +from .core.const import ( + CONF_DATABASE, + CONF_RADIO_TYPE, + CONF_ZIGPY, + DATA_ZHA, + DATA_ZHA_CONFIG, + DEFAULT_DATABASE_NAME, + EZSP_OVERWRITE_EUI64, + RadioType, +) + +# Only the common radio types will be autoprobed, ordered by new device popularity. +# XBee takes too long to probe since it scans through all possible bauds and likely has +# very few users to begin with. +AUTOPROBE_RADIOS = ( + RadioType.ezsp, + RadioType.znp, + RadioType.deconz, + RadioType.zigate, +) + +CONNECT_DELAY_S = 1.0 + +MIGRATION_RETRIES = 100 + +HARDWARE_DISCOVERY_SCHEMA = vol.Schema( + { + vol.Required("name"): str, + vol.Required("port"): dict, + vol.Required("radio_type"): str, + } +) + +HARDWARE_MIGRATION_SCHEMA = vol.Schema( + { + vol.Required("new_discovery_info"): HARDWARE_DISCOVERY_SCHEMA, + vol.Required("old_discovery_info"): vol.Schema( + { + vol.Exclusive("hw", "discovery"): HARDWARE_DISCOVERY_SCHEMA, + vol.Exclusive("usb", "discovery"): usb.UsbServiceInfo, + } + ), + } +) + +_LOGGER = logging.getLogger(__name__) + + +def _allow_overwrite_ezsp_ieee( + backup: zigpy.backups.NetworkBackup, +) -> zigpy.backups.NetworkBackup: + """Return a new backup with the flag to allow overwriting the EZSP EUI64.""" + new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) + new_stack_specific.setdefault("ezsp", {})[EZSP_OVERWRITE_EUI64] = True + + return backup.replace( + network_info=backup.network_info.replace(stack_specific=new_stack_specific) + ) + + +def _prevent_overwrite_ezsp_ieee( + backup: zigpy.backups.NetworkBackup, +) -> zigpy.backups.NetworkBackup: + """Return a new backup without the flag to allow overwriting the EZSP EUI64.""" + if "ezsp" not in backup.network_info.stack_specific: + return backup + + new_stack_specific = copy.deepcopy(backup.network_info.stack_specific) + new_stack_specific.setdefault("ezsp", {}).pop(EZSP_OVERWRITE_EUI64, None) + + return backup.replace( + network_info=backup.network_info.replace(stack_specific=new_stack_specific) + ) + + +class ZhaRadioManager: + """Helper class with radio related functionality.""" + + hass: HomeAssistant + + def __init__(self) -> None: + """Initialize ZhaRadioManager instance.""" + self.device_path: str | None = None + self.device_settings: dict[str, Any] | None = None + self.radio_type: RadioType | None = None + self.current_settings: zigpy.backups.NetworkBackup | None = None + self.backups: list[zigpy.backups.NetworkBackup] = [] + self.chosen_backup: zigpy.backups.NetworkBackup | None = None + + @contextlib.asynccontextmanager + async def _connect_zigpy_app(self) -> ControllerApplication: + """Connect to the radio with the current config and then clean up.""" + assert self.radio_type is not None + + config = self.hass.data.get(DATA_ZHA, {}).get(DATA_ZHA_CONFIG, {}) + app_config = config.get(CONF_ZIGPY, {}).copy() + + database_path = config.get( + CONF_DATABASE, + self.hass.config.path(DEFAULT_DATABASE_NAME), + ) + + # Don't create `zigbee.db` if it doesn't already exist + if not await self.hass.async_add_executor_job(os.path.exists, database_path): + database_path = None + + app_config[CONF_DATABASE] = database_path + app_config[CONF_DEVICE] = self.device_settings + app_config = self.radio_type.controller.SCHEMA(app_config) + + app = await self.radio_type.controller.new( + app_config, auto_form=False, start_radio=False + ) + + try: + await app.connect() + yield app + finally: + await app.disconnect() + await asyncio.sleep(CONNECT_DELAY_S) + + async def restore_backup( + self, backup: zigpy.backups.NetworkBackup, **kwargs: Any + ) -> None: + """Restore the provided network backup, passing through kwargs.""" + if self.current_settings is not None and self.current_settings.supersedes( + self.chosen_backup + ): + return + + async with self._connect_zigpy_app() as app: + await app.backups.restore_backup(backup, **kwargs) + + @staticmethod + def parse_radio_type(radio_type: str) -> RadioType: + """Parse a radio type name, accounting for past aliases.""" + if radio_type == "efr32": + return RadioType.ezsp + + return RadioType[radio_type] + + async def detect_radio_type(self) -> bool: + """Probe all radio types on the current port.""" + for radio in AUTOPROBE_RADIOS: + _LOGGER.debug("Attempting to probe radio type %s", radio) + + dev_config = radio.controller.SCHEMA_DEVICE( + {CONF_DEVICE_PATH: self.device_path} + ) + probe_result = await radio.controller.probe(dev_config) + + if not probe_result: + continue + + # Radio library probing can succeed and return new device settings + if isinstance(probe_result, dict): + dev_config = probe_result + + self.radio_type = radio + self.device_settings = dev_config + + return True + + return False + + async def async_load_network_settings( + self, *, create_backup: bool = False + ) -> zigpy.backups.NetworkBackup | None: + """Connect to the radio and load its current network settings.""" + backup = None + + async with self._connect_zigpy_app() as app: + # Check if the stick has any settings and load them + try: + await app.load_network_info() + except NetworkNotFormed: + pass + else: + self.current_settings = zigpy.backups.NetworkBackup( + network_info=app.state.network_info, + node_info=app.state.node_info, + ) + + if create_backup: + backup = await app.backups.create_backup() + + # The list of backups will always exist + self.backups = app.backups.backups.copy() + + return backup + + async def async_form_network(self) -> None: + """Form a brand new network.""" + async with self._connect_zigpy_app() as app: + await app.form_network() + + async def async_reset_adapter(self) -> None: + """Reset the current adapter.""" + async with self._connect_zigpy_app() as app: + await app.reset_network_info() + + async def async_restore_backup_step_1(self) -> bool: + """Prepare restoring backup. + + Returns True if async_restore_backup_step_2 should be called. + """ + assert self.chosen_backup is not None + + if self.radio_type != RadioType.ezsp: + await self.restore_backup(self.chosen_backup) + return False + + # We have no way to partially load network settings if no network is formed + if self.current_settings is None: + # Since we are going to be restoring the backup anyways, write it to the + # radio without overwriting the IEEE but don't take a backup with these + # temporary settings + temp_backup = _prevent_overwrite_ezsp_ieee(self.chosen_backup) + await self.restore_backup(temp_backup, create_new=False) + await self.async_load_network_settings() + + assert self.current_settings is not None + + if ( + self.current_settings.node_info.ieee == self.chosen_backup.node_info.ieee + or not self.current_settings.network_info.metadata["ezsp"][ + "can_write_custom_eui64" + ] + ): + # No point in prompting the user if the backup doesn't have a new IEEE + # address or if there is no way to overwrite the IEEE address a second time + await self.restore_backup(self.chosen_backup) + + return False + + return True + + async def async_restore_backup_step_2(self, overwrite_ieee: bool) -> None: + """Restore backup and optionally overwrite IEEE.""" + assert self.chosen_backup is not None + + backup = self.chosen_backup + + if overwrite_ieee: + backup = _allow_overwrite_ezsp_ieee(backup) + + # If the user declined to overwrite the IEEE *and* we wrote the backup to + # their empty radio above, restoring it again would be redundant. + await self.restore_backup(backup) + + +class ZhaMultiPANMigrationHelper: + """Helper class for automatic migration when upgrading the firmware of a radio. + + This class is currently only intended to be used when changing the firmware on the + radio used in the Home Assistant Sky Connect USB stick and the Home Asssistant Yellow + from Zigbee only firmware to firmware supporting both Zigbee and Thread. + """ + + def __init__( + self, hass: HomeAssistant, config_entry: config_entries.ConfigEntry + ) -> None: + """Initialize MigrationHelper instance.""" + self._config_entry = config_entry + self._hass = hass + self._radio_mgr = ZhaRadioManager() + self._radio_mgr.hass = hass + + async def async_initiate_migration(self, data: dict[str, Any]) -> bool: + """Initiate ZHA migration. + + The passed data should contain: + - Discovery data identifying the device being firmware updated + - Discovery data for connecting to the device after the firmware update is + completed. + + Returns True if async_finish_migration should be called after the firmware + update is completed. + """ + migration_data = HARDWARE_MIGRATION_SCHEMA(data) + + name = migration_data["new_discovery_info"]["name"] + new_radio_type = ZhaRadioManager.parse_radio_type( + migration_data["new_discovery_info"]["radio_type"] + ) + + new_device_settings = new_radio_type.controller.SCHEMA_DEVICE( + migration_data["new_discovery_info"]["port"] + ) + + if "hw" in migration_data["old_discovery_info"]: + old_device_path = migration_data["old_discovery_info"]["hw"]["port"]["path"] + else: # usb + device = migration_data["old_discovery_info"]["usb"].device + old_device_path = await self._hass.async_add_executor_job( + usb.get_serial_by_id, device + ) + + if self._config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] != old_device_path: + # ZHA is using another radio, do nothing + return False + + try: + await self._hass.config_entries.async_unload(self._config_entry.entry_id) + except config_entries.OperationNotAllowed: + # ZHA is not running + pass + + # Temporarily connect to the old radio to read its settings + config_entry_data = self._config_entry.data + old_radio_mgr = ZhaRadioManager() + old_radio_mgr.hass = self._hass + old_radio_mgr.device_path = config_entry_data[CONF_DEVICE][CONF_DEVICE_PATH] + old_radio_mgr.device_settings = config_entry_data[CONF_DEVICE] + old_radio_mgr.radio_type = RadioType[config_entry_data[CONF_RADIO_TYPE]] + backup = await old_radio_mgr.async_load_network_settings(create_backup=True) + + # Then configure the radio manager for the new radio to use the new settings + self._radio_mgr.chosen_backup = backup + self._radio_mgr.radio_type = new_radio_type + self._radio_mgr.device_path = new_device_settings[CONF_DEVICE_PATH] + self._radio_mgr.device_settings = new_device_settings + device_settings = self._radio_mgr.device_settings.copy() # type: ignore[union-attr] + + # Update the config entry settings + self._hass.config_entries.async_update_entry( + entry=self._config_entry, + data={ + CONF_DEVICE: device_settings, + CONF_RADIO_TYPE: self._radio_mgr.radio_type.name, + }, + options=self._config_entry.options, + title=name, + ) + return True + + async def async_finish_migration(self) -> None: + """Finish ZHA migration. + + Throws an exception if the migration did not succeed. + """ + # Restore the backup, permanently overwriting the device IEEE address + for retry in range(MIGRATION_RETRIES): + try: + if await self._radio_mgr.async_restore_backup_step_1(): + await self._radio_mgr.async_restore_backup_step_2(True) + + break + except OSError as err: + if retry >= MIGRATION_RETRIES - 1: + raise + + _LOGGER.debug( + "Failed to restore backup %r, retrying in %s seconds", + err, + CONNECT_DELAY_S, + ) + + await asyncio.sleep(CONNECT_DELAY_S) + + _LOGGER.debug("Restored backup after %s retries", retry) + + # Launch ZHA again + try: + await self._hass.config_entries.async_setup(self._config_entry.entry_id) + except config_entries.OperationNotAllowed: + # ZHA is not unloaded + pass diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 5ac0ec6d164..295c61314c7 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -269,6 +269,26 @@ class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_stat _attr_name = "Power on state" +class TuyaBacklightMode(types.enum8): + """Tuya switch backlight mode enum.""" + + Off = 0x00 + LightWhenOn = 0x01 + LightWhenOff = 0x02 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_ON_OFF, + models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"}, +) +class TuyaBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"): + """Representation of a ZHA backlight mode select entity.""" + + _select_attr = "backlight_mode" + _enum = TuyaBacklightMode + _attr_name = "Backlight mode" + + class MoesBacklightMode(types.enum8): """MOES switch backlight mode enum.""" @@ -316,7 +336,8 @@ class AqaraMotionSensitivities(types.enum8): @CONFIG_DIAGNOSTIC_MATCH( - channel_names="opple_cluster", models={"lumi.motion.ac01", "lumi.motion.ac02"} + channel_names="opple_cluster", + models={"lumi.motion.ac01", "lumi.motion.ac02", "lumi.motion.agl04"}, ) class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"): """Representation of a ZHA motion sensitivity configuration entity.""" @@ -456,3 +477,20 @@ class InovelliSwitchTypeEntity(ZCLEnumSelectEntity, id_suffix="switch_type"): _select_attr = "switch_type" _enum = InovelliSwitchType _attr_name: str = "Switch type" + + +class AqaraFeedingMode(types.enum8): + """Feeding mode.""" + + Manual = 0x00 + Schedule = 0x01 + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederMode(ZCLEnumSelectEntity, id_suffix="feeding_mode"): + """Representation of an Aqara pet feeder mode configuration entity.""" + + _select_attr = "feeding_mode" + _enum = AqaraFeedingMode + _attr_name = "Mode" + _attr_icon: str = "mdi:wrench-clock" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index ba4aec66f35..003f5771b93 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -5,6 +5,8 @@ import functools import numbers from typing import TYPE_CHECKING, Any, TypeVar +from zigpy import types + from homeassistant.components.climate import HVACAction from homeassistant.components.sensor import ( SensorDeviceClass, @@ -36,6 +38,7 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, Platform, + UnitOfMass, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -837,3 +840,53 @@ class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): _attr_icon = "mdi:timer" _attr_name: str = "Filter run time" _unit = TIME_MINUTES + + +class AqaraFeedingSource(types.enum8): + """Aqara pet feeder feeding source.""" + + Feeder = 0x01 + HomeAssistant = 0x02 + + +@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederLastFeedingSource(Sensor, id_suffix="last_feeding_source"): + """Sensor that displays the last feeding source of pet feeder.""" + + SENSOR_ATTR = "last_feeding_source" + _attr_name: str = "Last feeding source" + _attr_icon = "mdi:devices" + + def formatter(self, value: int) -> int | float | None: + """Numeric pass-through formatter.""" + return AqaraFeedingSource(value).name + + +@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederLastFeedingSize(Sensor, id_suffix="last_feeding_size"): + """Sensor that displays the last feeding size of the pet feeder.""" + + SENSOR_ATTR = "last_feeding_size" + _attr_name: str = "Last feeding size" + _attr_icon: str = "mdi:counter" + + +@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederPortionsDispensed(Sensor, id_suffix="portions_dispensed"): + """Sensor that displays the number of portions dispensed by the pet feeder.""" + + SENSOR_ATTR = "portions_dispensed" + _attr_name: str = "Portions dispensed today" + _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING + _attr_icon: str = "mdi:counter" + + +@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederWeightDispensed(Sensor, id_suffix="weight_dispensed"): + """Sensor that displays the weight weight dispensed by the pet feeder.""" + + SENSOR_ATTR = "weight_dispensed" + _attr_name: str = "Weight dispensed today" + _unit = UnitOfMass.GRAMS + _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING + _attr_icon: str = "mdi:weight-gram" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 0c2e5e7ebe2..c9469747a3f 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -174,7 +174,8 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): _attr_entity_category = EntityCategory.CONFIG _zcl_attribute: str - _zcl_inverter_attribute: str = "" + _zcl_inverter_attribute: str | None = None + _force_inverted: bool = False @classmethod def create_entity( @@ -225,19 +226,24 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): """Handle state update from channel.""" self.async_write_ha_state() + @property + def inverted(self) -> bool: + """Return True if the switch is inverted.""" + if self._zcl_inverter_attribute: + return bool(self._channel.cluster.get(self._zcl_inverter_attribute)) + return self._force_inverted + @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" val = bool(self._channel.cluster.get(self._zcl_attribute)) - invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) - return (not val) if invert else val + return (not val) if self.inverted else val async def async_turn_on_off(self, state: bool) -> None: """Turn the entity on or off.""" try: - invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) result = await self._channel.cluster.write_attributes( - {self._zcl_attribute: not state if invert else state} + {self._zcl_attribute: not state if self.inverted else state} ) except zigpy.exceptions.ZigbeeException as ex: self.error("Could not set value: %s", ex) @@ -258,15 +264,15 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): async def async_update(self) -> None: """Attempt to retrieve the state of the entity.""" await super().async_update() - _LOGGER.error("Polling current state") + self.error("Polling current state") if self._channel: value = await self._channel.get_attribute_value( self._zcl_attribute, from_cache=False ) - invert = await self._channel.get_attribute_value( + await self._channel.get_attribute_value( self._zcl_inverter_attribute, from_cache=False ) - _LOGGER.debug("read value=%s, inverter=%s", value, bool(invert)) + self.debug("read value=%s, inverted=%s", value, self.inverted) @CONFIG_DIAGNOSTIC_MATCH( @@ -430,3 +436,36 @@ class InovelliDisableDoubleTapClearNotificationsMode( _zcl_attribute: str = "disable_clear_notifications_double_tap" _attr_name: str = "Disable config 2x tap to clear notifications" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederLEDIndicator( + ZHASwitchConfigurationEntity, id_suffix="disable_led_indicator" +): + """Representation of a LED indicator configuration entity.""" + + _zcl_attribute: str = "disable_led_indicator" + _attr_name = "LED indicator" + _force_inverted = True + _attr_icon: str = "mdi:led-on" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) +class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"): + """Representation of a child lock configuration entity.""" + + _zcl_attribute: str = "child_lock" + _attr_name = "Child lock" + _attr_icon: str = "mdi:account-lock" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_ON_OFF, + models={"TS011F"}, +) +class TuyaChildLockSwitch(ZHASwitchConfigurationEntity, id_suffix="child_lock"): + """Representation of a child lock configuration entity.""" + + _zcl_attribute: str = "child_lock" + _attr_name = "Child lock" + _attr_icon: str = "mdi:account-lock" diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 1c4c44c9dff..3fa4c4fbdf5 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -2,10 +2,10 @@ "config": { "abort": { "not_zha_device": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 zha \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 ZHA." + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { - "cannot_connect": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 ZHA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "{name}", "step": { @@ -39,26 +39,10 @@ "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442" }, - "pick_radio": { - "data": { - "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" - }, - "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" - }, - "port_config": { - "data": { - "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b" } - }, - "user": { - "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442 \u0437\u0430 Zigbee \u0440\u0430\u0434\u0438\u043e", - "title": "ZHA" } } }, @@ -100,18 +84,27 @@ "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0438\u0437\u0442\u044a\u0440\u0432\u0430\u043d\u043e", "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043e\u0431\u044a\u0440\u043d\u0430\u0442\u043e \"{subtype}\"", "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \"{subtype}\"", + "device_offline": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043e\u0444\u043b\u0430\u0439\u043d", "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \"{subtype}\"", "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0433\u043e \u0435 \u043f\u043b\u044a\u0437\u043d\u0430\u0442\u043e \"{subtype}\"", "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0435\u043d\u043e", - "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", - "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", - "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", - "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", - "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e" + "remote_button_alt_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "remote_button_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435" } }, "options": { @@ -119,9 +112,6 @@ "not_zha_device": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 zha \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, - "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" - }, "flow_title": "{name}", "step": { "choose_formation_strategy": { @@ -149,7 +139,7 @@ "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" }, - "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0432\u043e\u044f \u0442\u0438\u043f Zigbee \u0440\u0430\u0434\u0438\u043e", + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0442\u0438\u043f\u0430 \u043d\u0430 \u0432\u0430\u0448\u0435\u0442\u043e Zigbee \u0440\u0430\u0434\u0438\u043e", "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" }, "manual_port_config": { diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index db5d77047f5..22ae3910518 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -64,35 +64,12 @@ "description": "La teva c\u00f2pia de seguretat t\u00e9 una adre\u00e7a IEEE diferent de la teva r\u00e0dio. Perqu\u00e8 la xarxa funcioni correctament, tamb\u00e9 s'ha de canviar l'adre\u00e7a IEEE de la teva r\u00e0dio. \n\nAquesta \u00e9s una operaci\u00f3 permanent.", "title": "Sobreescriu l'adre\u00e7a IEEE r\u00e0dio" }, - "pick_radio": { - "data": { - "radio_type": "Tipus de r\u00e0dio" - }, - "description": "Tria el teu tipus de r\u00e0dio Zigbee", - "title": "Tipus de r\u00e0dio" - }, - "port_config": { - "data": { - "baudrate": "velocitat del port", - "flow_control": "control de flux de dades", - "path": "Ruta del port s\u00e8rie al dispositiu" - }, - "description": "Introdueix la configuraci\u00f3 espec\u00edfica de port", - "title": "Configuraci\u00f3" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Puja un fitxer" }, "description": "Restaura la configuraci\u00f3 de xarxa des d'un fitxer JSON de c\u00f2pia de seguretat penjat. Pots baixar-ne un des d'una instal\u00b7laci\u00f3 ZHA diferent anant a **Configuraci\u00f3 de xarxa** o utilitzar un fitxer `coordinator_backup.json` de Zigbee2MQTT.", "title": "Pujada de c\u00f2pia de seguretat manual" - }, - "user": { - "data": { - "path": "Ruta del port s\u00e8rie al dispositiu" - }, - "description": "Selecciona el port s\u00e8rie per a la r\u00e0dio Zigbee", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 13e8eabddad..42a473abf96 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -41,25 +41,12 @@ }, "title": "P\u0159epsat adresu IEEE r\u00e1dia" }, - "pick_radio": { - "description": "Vyberte typ sv\u00e9ho r\u00e1dia Zigbee" - }, - "port_config": { - "data": { - "baudrate": "rychlost portu" - }, - "title": "Nastaven\u00ed" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Nahr\u00e1t soubor" }, "description": "Obnovit nastaven\u00ed s\u00edt\u011b z nahran\u00e9ho z\u00e1lo\u017en\u00edho souboru JSON. Soubor m\u016f\u017eete st\u00e1hnout z jin\u00e9 instalace ZHA v **Nastaven\u00ed s\u00edt\u011b** nebo pou\u017eijte soubor `coordinator_backup.json` z integrace Zigbee2MQTT.", "title": "Nahr\u00e1t ru\u010dn\u00ed z\u00e1lohu" - }, - "user": { - "description": "Vyberte s\u00e9riov\u00fd port pro r\u00e1dio Zigbee", - "title": "ZHA" } } }, @@ -155,6 +142,9 @@ }, "title": "P\u0159epsat adresu IEEE r\u00e1dia" }, + "prompt_migrate_or_reconfigure": { + "description": "P\u0159ech\u00e1z\u00edte na nov\u00e9 r\u00e1dio nebo m\u011bn\u00edte konfiguraci st\u00e1vaj\u00edc\u00edho r\u00e1dia?" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Nahr\u00e1t soubor" diff --git a/homeassistant/components/zha/translations/da.json b/homeassistant/components/zha/translations/da.json index 6a6e34c2581..5914cd9d53d 100644 --- a/homeassistant/components/zha/translations/da.json +++ b/homeassistant/components/zha/translations/da.json @@ -5,31 +5,6 @@ }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til ZHA-enhed." - }, - "step": { - "pick_radio": { - "data": { - "radio_type": "Radiotype" - }, - "description": "V\u00e6lg en type Zigbee-radio", - "title": "Radiotype" - }, - "port_config": { - "data": { - "baudrate": "porthastighed", - "flow_control": "dataflowstyring", - "path": "Sti til seriel enhed" - }, - "description": "Angiv portspecifikke indstillinger", - "title": "Indstillinger" - }, - "user": { - "data": { - "path": "Stien til seriel enhed" - }, - "description": "V\u00e6lg seriel port til Zigbee-radio", - "title": "ZHA" - } } }, "device_automation": { diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 5be0add1ae2..40112b0c36c 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -64,35 +64,12 @@ "description": "Dein Backup hat eine andere IEEE-Adresse als dein Funkger\u00e4t. Damit dein Netzwerk ordnungsgem\u00e4\u00df funktioniert, sollte auch die IEEE-Adresse deines Funkger\u00e4ts ge\u00e4ndert werden.\n\nDies ist ein permanenter Vorgang.", "title": "Funk-IEEE-Adresse \u00fcberschreiben" }, - "pick_radio": { - "data": { - "radio_type": "Funktyp" - }, - "description": "W\u00e4hle den Typ deines Zigbee-Funks", - "title": "Funktyp" - }, - "port_config": { - "data": { - "baudrate": "Port-Geschwindigkeit", - "flow_control": "Datenflusskontrolle", - "path": "Serieller Ger\u00e4tepfad" - }, - "description": "Gib die portspezifischen Einstellungen ein", - "title": "Einstellungen" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Datei hochladen" }, - "description": "Stelle deine Netzwerkeinstellungen aus einer hochgeladenen Backup-JSON-Datei wieder her. Du kannst eine von einer anderen ZHA-Installation unter **Netzwerkeinstellungen** herunterladen oder eine Zigbee2MQTT-Datei \"coordinator_backup.json\" verwenden.", + "description": "Stelle deine Netzwerkeinstellungen aus einer hochgeladenen Backup-JSON-Datei wieder her. Du kannst eine von einer anderen ZHA Installation unter **Netzwerkeinstellungen** herunterladen oder eine Zigbee2MQTT-Datei \"coordinator_backup.json\" verwenden.", "title": "Manuelles Backup hochladen" - }, - "user": { - "data": { - "path": "Serieller Ger\u00e4tepfad" - }, - "description": "W\u00e4hle die serielle Schnittstelle f\u00fcr den ZigBee-Funk", - "title": "ZHA" } } }, @@ -104,7 +81,7 @@ "title": "Optionen f\u00fcr die Alarmsteuerung" }, "zha_options": { - "always_prefer_xy_color_mode": "Immer den XY-Farbmodus bevorzugen", + "always_prefer_xy_color_mode": "Immer den XY Farbmodus bevorzugen", "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", @@ -255,7 +232,7 @@ "data": { "uploaded_backup_file": "Datei hochladen" }, - "description": "Stelle deine Netzwerkeinstellungen aus einer hochgeladenen Backup-JSON-Datei wieder her. Du kannst eine von einer anderen ZHA-Installation unter **Netzwerkeinstellungen** herunterladen oder eine Zigbee2MQTT-Datei \"coordinator_backup.json\" verwenden.", + "description": "Stelle deine Netzwerkeinstellungen aus einer hochgeladenen Backup-JSON-Datei wieder her. Du kannst eine von einer anderen ZHA Installation unter **Netzwerkeinstellungen** herunterladen oder eine Zigbee2MQTT-Datei \"coordinator_backup.json\" verwenden.", "title": "Manuelles Backup hochladen" } } diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 2d3ca09560c..3a85b35b27b 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -64,35 +64,12 @@ "description": "\u03a4\u03bf \u03b5\u03c6\u03b5\u03b4\u03c1\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03cc \u03c3\u03b1\u03c2. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03c9\u03c3\u03c4\u03ac \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9 \u03ba\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03c4\u03bf\u03c5 \u03c1\u03b1\u03b4\u03b9\u03bf\u03c6\u03ce\u03bd\u03bf\u03c5 \u03c3\u03b1\u03c2.\n\n\u0391\u03c5\u03c4\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.", "title": "\u0391\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE Radio" }, - "pick_radio": { - "data": { - "radio_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03bd\u03cc\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c5 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2 Zigbee", - "title": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" - }, - "port_config": { - "data": { - "baudrate": "\u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03b8\u03cd\u03c1\u03b1\u03c2", - "flow_control": "\u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", - "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" - }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03b8\u03cd\u03c1\u03b1\u03c2", - "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf" }, "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ad\u03bd\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03c4\u03c9\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03c5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 JSON. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03bb\u03ae\u03c8\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c0\u03cc \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 ZHA \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 **\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5** \u03ae \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u00abcoordinator_backup.json\u00bb Zigbee2MQTT.", "title": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" - }, - "user": { - "data": { - "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1 Zigbee", - "title": "\u0396\u0397\u0391" } } }, diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index f624f4d5499..bb62ccca64a 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -64,35 +64,12 @@ "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "title": "Overwrite Radio IEEE Address" }, - "pick_radio": { - "data": { - "radio_type": "Radio Type" - }, - "description": "Pick a type of your Zigbee radio", - "title": "Radio Type" - }, - "port_config": { - "data": { - "baudrate": "port speed", - "flow_control": "data flow control", - "path": "Serial device path" - }, - "description": "Enter port specific settings", - "title": "Settings" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Upload a file" }, "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", "title": "Upload a Manual Backup" - }, - "user": { - "data": { - "path": "Serial Device Path" - }, - "description": "Select serial port for Zigbee radio", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/es-419.json b/homeassistant/components/zha/translations/es-419.json index f2aaf80c78a..ae67dc82444 100644 --- a/homeassistant/components/zha/translations/es-419.json +++ b/homeassistant/components/zha/translations/es-419.json @@ -5,11 +5,6 @@ }, "error": { "cannot_connect": "No se puede conectar al dispositivo ZHA." - }, - "step": { - "user": { - "title": "ZHA" - } } }, "device_automation": { diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 2919302a1ac..3b5315f2fbd 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -64,35 +64,12 @@ "description": "Tu copia de seguridad tiene una direcci\u00f3n IEEE diferente a la de tu radio. Para que tu red funcione correctamente, tambi\u00e9n debes cambiar la direcci\u00f3n IEEE de tu radio. \n\nEsta es una operaci\u00f3n permanente.", "title": "Sobrescribir la direcci\u00f3n IEEE de la radio" }, - "pick_radio": { - "data": { - "radio_type": "Tipo de Radio" - }, - "description": "Selecciona el tipo de tu radio Zigbee", - "title": "Tipo de Radio" - }, - "port_config": { - "data": { - "baudrate": "velocidad del puerto", - "flow_control": "control de flujo de datos", - "path": "Ruta del dispositivo serie" - }, - "description": "Introduce los ajustes espec\u00edficos del puerto", - "title": "Configuraci\u00f3n" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Subir un archivo" }, "description": "Restaura la configuraci\u00f3n de tu red desde un archivo JSON de copia de seguridad subido. Puedes descargar uno de una instalaci\u00f3n diferente de ZHA desde **Configuraci\u00f3n de red**, o usar un archivo `coordinator_backup.json` de Zigbee2MQTT.", "title": "Subir una copia de seguridad manual" - }, - "user": { - "data": { - "path": "Ruta del Dispositivo Serie" - }, - "description": "Selecciona puerto serie para radio Zigbee", - "title": "ZHA" } } }, @@ -150,7 +127,7 @@ "device_flipped": "Dispositivo volteado \"{subtype}\"", "device_knocked": "Dispositivo golpeado \"{subtype}\"", "device_offline": "Dispositivo sin conexi\u00f3n", - "device_rotated": "Dispositivo girado \"{subtype}\"", + "device_rotated": "Dispositivo rotado \"{subtype}\"", "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \"{subtype}\"", "device_tilted": "Dispositivo inclinado", diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 54cabbe4c82..d0e8a062120 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -64,35 +64,12 @@ "description": "Varukoopial on erinev IEEE aadress kui raadiol. V\u00f5rgu n\u00f5uetekohaseks toimimiseks tuleks muuta ka raadio IEEE aadressi.\n\nSee on p\u00fcsiv toiming.", "title": "Raadio IEEE aadressi \u00fclekirjutamine" }, - "pick_radio": { - "data": { - "radio_type": "Seadme raadio t\u00fc\u00fcp" - }, - "description": "Vali oma Zigbee raadio t\u00fc\u00fcp", - "title": "Seadme raadio t\u00fc\u00fcp" - }, - "port_config": { - "data": { - "baudrate": "pordi kiirus", - "flow_control": "andmevoo juhtimine", - "path": "Jadaseadme tee" - }, - "description": "Sisesta pordispetsiifilised seaded", - "title": "Seaded" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Faili \u00fcleslaadimine" }, "description": "Taasta oma v\u00f5rgus\u00e4tted \u00fcleslaaditud JSON-varufailist. Saad selle alla laadida teisest ZHA paigaldusest **V\u00f5rguseaded** v\u00f5i kasutada Zigbee2MQTT 'coordinator_backup.json' faili.", "title": "K\u00e4sitsi varundamise \u00fcleslaadimine" - }, - "user": { - "data": { - "path": "Jadaseadme tee" - }, - "description": "Vali Zigbee raadio jadaport", - "title": "" } } }, diff --git a/homeassistant/components/zha/translations/fi.json b/homeassistant/components/zha/translations/fi.json index e22705142d4..c027787ebc2 100644 --- a/homeassistant/components/zha/translations/fi.json +++ b/homeassistant/components/zha/translations/fi.json @@ -2,27 +2,6 @@ "config": { "error": { "cannot_connect": "Yhteyden muodostaminen ZHA-laitteeseen ei onnistu." - }, - "step": { - "pick_radio": { - "data": { - "radio_type": "Radiotyyppi" - }, - "description": "Valitse Zigbee-radion tyyppi", - "title": "Radion tyyppi" - }, - "port_config": { - "data": { - "baudrate": "portin nopeus", - "flow_control": "tietovirran hallinta", - "path": "Sarjalaitteen polku" - }, - "description": "Anna porttikohtaiset asetukset", - "title": "Asetukset" - }, - "user": { - "title": "ZHA" - } } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 505c6780d6a..0fccc144fb1 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -61,34 +61,11 @@ }, "title": "\u00c9craser l'adresse IEEE de la radio" }, - "pick_radio": { - "data": { - "radio_type": "Type de radio" - }, - "description": "Choisissez un type de radio Zigbee", - "title": "Type de radio" - }, - "port_config": { - "data": { - "baudrate": "vitesse du port", - "flow_control": "contr\u00f4le du flux de donn\u00e9es", - "path": "Chemin du p\u00e9riph\u00e9rique s\u00e9rie" - }, - "description": "Saisir les param\u00e8tres sp\u00e9cifiques au port", - "title": "R\u00e9glages" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "T\u00e9l\u00e9verser un fichier" }, "title": "T\u00e9l\u00e9verser une sauvegarde manuelle" - }, - "user": { - "data": { - "path": "Chemin du p\u00e9riph\u00e9rique s\u00e9rie" - }, - "description": "S\u00e9lectionnez le port s\u00e9rie de la radio Zigbee", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/he.json b/homeassistant/components/zha/translations/he.json index f48b30bd826..ed0117bcbe3 100644 --- a/homeassistant/components/zha/translations/he.json +++ b/homeassistant/components/zha/translations/he.json @@ -8,17 +8,11 @@ }, "flow_title": "{name}", "step": { - "port_config": { - "title": "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05e7\u05d5\u05d1\u05e5" }, "title": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05d2\u05d9\u05d1\u05d5\u05d9 \u05d9\u05d3\u05e0\u05d9" - }, - "user": { - "title": "ZHA" } } }, @@ -57,6 +51,9 @@ "abort": { "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "flow_title": "{name}", "step": { "init": { diff --git a/homeassistant/components/zha/translations/hr.json b/homeassistant/components/zha/translations/hr.json new file mode 100644 index 00000000000..e76ee16fc0c --- /dev/null +++ b/homeassistant/components/zha/translations/hr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ve\u0107 konfigurirano. Mogu\u0107a samo jedna konfiguracija." + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + } + }, + "options": { + "abort": { + "single_instance_allowed": "Ve\u0107 konfigurirano. Mogu\u0107a samo jedna konfiguracija." + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + }, + "step": { + "intent_migrate": { + "title": "Migracija na novi radio" + }, + "prompt_migrate_or_reconfigure": { + "description": "Migrirate li na novi radio ili rekonfigurirate trenutni radio?", + "menu_options": { + "intent_migrate": "Migracija na novi radio", + "intent_reconfigure": "Ponovno konfiguriranje trenutnog radija" + }, + "title": "Migracija ili ponovna konfiguracija" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index b70bfcd597b..8b596df47d7 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -64,35 +64,12 @@ "description": "A biztons\u00e1gi m\u00e1solat IEEE-c\u00edme elt\u00e9r a r\u00e1di\u00f3\u00e9t\u00f3l. A h\u00e1l\u00f3zat megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9hez a r\u00e1di\u00f3 IEEE-c\u00edm\u00e9t is meg kell v\u00e1ltoztatni. \n\n Ez egy v\u00e9gleles m\u0171velet.", "title": "A r\u00e1di\u00f3 IEEE-c\u00edm\u00e9nek fel\u00fcl\u00edr\u00e1sa" }, - "pick_radio": { - "data": { - "radio_type": "R\u00e1di\u00f3 t\u00edpusa" - }, - "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 t\u00edpus\u00e1t", - "title": "R\u00e1di\u00f3 t\u00edpusa" - }, - "port_config": { - "data": { - "baudrate": "port sebess\u00e9g", - "flow_control": "adat\u00e1raml\u00e1s szab\u00e1lyoz\u00e1sa", - "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" - }, - "description": "Adja meg a port specifikus be\u00e1ll\u00edt\u00e1sokat", - "title": "Be\u00e1ll\u00edt\u00e1sok" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "F\u00e1jl felt\u00f6lt\u00e9se" }, "description": "H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok vissza\u00e1ll\u00edt\u00e1sa egy felt\u00f6lt\u00f6tt JSON biztons\u00e1gi m\u00e1solatb\u00f3l. Let\u00f6lthet egyet egy m\u00e1sik ZHA telep\u00edt\u00e9sb\u0151l a **H\u00e1l\u00f3zati be\u00e1ll\u00edt\u00e1sok** men\u00fcpontb\u00f3l, vagy haszn\u00e1lhat egy Zigbee2MQTT `coordinator_backup.json` f\u00e1jlt.", "title": "K\u00e9zi biztons\u00e1gi m\u00e1solat felt\u00f6lt\u00e9se" - }, - "user": { - "data": { - "path": "Soros eszk\u00f6z el\u00e9r\u00e9si \u00fatja" - }, - "description": "V\u00e1lassza ki a Zigbee r\u00e1di\u00f3 soros portj\u00e1t", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index ab496b3be53..9ede5ff9d98 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -61,38 +61,15 @@ "data": { "overwrite_coordinator_ieee": "Ganti alamat radio IEEE secara permanen" }, - "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", + "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", "title": "Timpa Alamat IEEE Radio" }, - "pick_radio": { - "data": { - "radio_type": "Jenis Radio" - }, - "description": "Pilih jenis radio Zigbee Anda", - "title": "Jenis Radio" - }, - "port_config": { - "data": { - "baudrate": "kecepatan port", - "flow_control": "kontrol data flow", - "path": "Jalur perangkat serial" - }, - "description": "Masukkan pengaturan khusus port", - "title": "Setelan" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Unggah file" }, "description": "Pulihkan pengaturan jaringan Anda dari file JSON cadangan yang diunggah. Anda dapat mengunduhnya dari instalasi ZHA yang berbeda dari **Pengaturan Jaringan**, atau menggunakan file Zigbee2MQTT 'coordinator_backup.json'.", "title": "Unggah Cadangan Manual" - }, - "user": { - "data": { - "path": "Jalur Perangkat Serial" - }, - "description": "Pilih port serial untuk radio Zigbee", - "title": "ZHA" } } }, @@ -207,7 +184,7 @@ "title": "Pilih Port Serial" }, "init": { - "description": "ZHA akan dihentikan. Ingin melanjutkan?", + "description": "ZHA akan dihentikan. Ingin melanjutkan?", "title": "Konfigurasi Ulang ZHA" }, "instruct_unplug": { @@ -215,7 +192,7 @@ "title": "Cabut radio lama Anda" }, "intent_migrate": { - "description": "Radio lama Anda akan disetel ulang ke setelan pabrikan. Jika Anda menggunakan adaptor gabungan Z-Wave dan Zigbee seperti HUSBZB-1, ini hanya akan mengatur ulang bagian Zigbee.\n\nApakah Anda ingin melanjutkan?", + "description": "Radio lama Anda akan disetel ulang ke setelan pabrikan. Jika Anda menggunakan adaptor gabungan Z-Wave dan Zigbee seperti HUSBZB-1, ini hanya akan mengatur ulang bagian Zigbee.\n\nApakah Anda ingin melanjutkan?", "title": "Migrasikan ke radio baru" }, "manual_pick_radio_type": { @@ -238,7 +215,7 @@ "data": { "overwrite_coordinator_ieee": "Ganti alamat radio IEEE secara permanen" }, - "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", + "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", "title": "Timpa Alamat IEEE Radio" }, "prompt_migrate_or_reconfigure": { diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 02b3549d263..56f0f28e7f6 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -64,35 +64,12 @@ "description": "Il tuo backup ha un indirizzo IEEE diverso dalla tua radio. Affinch\u00e9 la rete funzioni correttamente, \u00e8 necessario modificare anche l'indirizzo IEEE della radio. \n\nQuesta \u00e8 un'operazione permanente.", "title": "Sovrascrivi indirizzo IEEE radio" }, - "pick_radio": { - "data": { - "radio_type": "Tipo di radio" - }, - "description": "Scegli un tipo di radio Zigbee", - "title": "Tipo di radio" - }, - "port_config": { - "data": { - "baudrate": "velocit\u00e0 della porta", - "flow_control": "controllo del flusso di dati", - "path": "Percorso del dispositivo seriale" - }, - "description": "Inserire le impostazioni specifiche della porta", - "title": "Impostazioni" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Carica un file" }, "description": "Ripristina le impostazioni di rete da un file JSON di backup caricato. Puoi scaricarne uno da un'installazione ZHA diversa da **Impostazioni di rete** o utilizzare un file Zigbee2MQTT `coordinator_backup.json`.", "title": "Carica un backup manuale" - }, - "user": { - "data": { - "path": "Percorso del dispositivo seriale" - }, - "description": "Seleziona la porta seriale per la radio Zigbee", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 5cf05092122..030c38106c5 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -64,35 +64,12 @@ "description": "\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u306b\u306f\u3001\u7121\u7dda\u3068\u306f\u7570\u306a\u308bIEEE\u30a2\u30c9\u30ec\u30b9\u304c\u3042\u308a\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u304c\u6b63\u3057\u304f\u6a5f\u80fd\u3059\u308b\u306b\u306f\u3001\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u3082\u5909\u66f4\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u308c\u306f\u6052\u4e45\u7684\u306a\u64cd\u4f5c\u3067\u3059\u3002", "title": "\u7121\u7dda\u306eIEEE\u30a2\u30c9\u30ec\u30b9\u306e\u4e0a\u66f8\u304d" }, - "pick_radio": { - "data": { - "radio_type": "\u7121\u7dda\u30bf\u30a4\u30d7" - }, - "description": "Zigbee\u7121\u7dda\u6a5f\u306e\u30bf\u30a4\u30d7\u3092\u9078\u3076", - "title": "\u7121\u7dda\u30bf\u30a4\u30d7" - }, - "port_config": { - "data": { - "baudrate": "\u30dd\u30fc\u30c8\u901f\u5ea6", - "flow_control": "\u30c7\u30fc\u30bf\u30d5\u30ed\u30fc\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb", - "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "description": "\u30dd\u30fc\u30c8\u56fa\u6709\u306e\u8a2d\u5b9a\u3092\u5165\u529b", - "title": "\u8a2d\u5b9a" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" }, "description": "\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3055\u308c\u305f\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 JSON \u30d5\u30a1\u30a4\u30eb\u304b\u3089\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u8a2d\u5b9a\u3092\u5fa9\u5143\u3057\u307e\u3059\u3002 **Network Settings** \u304b\u3089\u5225\u306e ZHA \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304b\u3089\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u304b\u3001Zigbee2MQTT `coordinator_backup.json` \u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002", "title": "\u624b\u52d5\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b" - }, - "user": { - "data": { - "path": "\u30b7\u30ea\u30a2\u30eb \u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "description": "Zigbee radio\u7528\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u9078\u629e", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/ko.json b/homeassistant/components/zha/translations/ko.json index 89dfaeba9be..f537347d9cd 100644 --- a/homeassistant/components/zha/translations/ko.json +++ b/homeassistant/components/zha/translations/ko.json @@ -6,32 +6,7 @@ "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "ZHA: {name}", - "step": { - "pick_radio": { - "data": { - "radio_type": "\ubb34\uc120 \uc720\ud615" - }, - "description": "\uc9c0\uadf8\ube44 \ubb34\uc120 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "\ubb34\uc120 \uc720\ud615" - }, - "port_config": { - "data": { - "baudrate": "\ud3ec\ud2b8 \uc18d\ub3c4", - "flow_control": "\ub370\uc774\ud130 \ud750\ub984 \uc81c\uc5b4", - "path": "\uc2dc\ub9ac\uc5bc \uc7a5\uce58 \uacbd\ub85c" - }, - "description": "\uac01 \ud3ec\ud2b8\ubcc4 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "\uc124\uc815\ub0b4\uc6a9" - }, - "user": { - "data": { - "path": "\uc2dc\ub9ac\uc5bc \uc7a5\uce58 \uacbd\ub85c" - }, - "description": "Zigbee \ubb34\uc120 \uc7a5\uce58\uc758 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "ZHA" - } - } + "flow_title": "ZHA: {name}" }, "device_automation": { "action_type": { diff --git a/homeassistant/components/zha/translations/lb.json b/homeassistant/components/zha/translations/lb.json index 3e076eca496..efb94a2539f 100644 --- a/homeassistant/components/zha/translations/lb.json +++ b/homeassistant/components/zha/translations/lb.json @@ -5,31 +5,6 @@ }, "error": { "cannot_connect": "Feeler beim verbannen" - }, - "step": { - "pick_radio": { - "data": { - "radio_type": "Typ vun Radio" - }, - "description": "Typ vum Zigbee Radio auswielen", - "title": "Typ vun Radio" - }, - "port_config": { - "data": { - "baudrate": "Vitesse vum Port", - "flow_control": "Data Flow Kontroll", - "path": "Pad zum seriellen Apparat" - }, - "description": "G\u00ebff spezifesch Port Astellungen an.", - "title": "Astellungen" - }, - "user": { - "data": { - "path": "Pad zum seriellen Apparat" - }, - "description": "Serielle Port fir Zigbee Radio auswielen", - "title": "ZHA" - } } }, "device_automation": { diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index e408a9949bc..f47f71da21c 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -35,34 +35,12 @@ "baudrate": "poortsnelheid" } }, - "pick_radio": { - "data": { - "radio_type": "Radio type" - }, - "description": "Kies een type Zigbee-radio", - "title": "Radio type" - }, - "port_config": { - "data": { - "baudrate": "poort snelheid", - "flow_control": "gegevensstroombeheer", - "path": "Serieel apparaatpad" - }, - "description": "Voer poortspecifieke instellingen in", - "title": "Instellingen" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Een bestand uploaden" }, + "description": "Herstel je netwerkinstellingen van ge-upload backup-JSON-bestand. Je kunt deze dpwnloaden van een andere ZHA installatie via **Netwerkinstellingen**, of gebruik een Zigbee2MQTT `coordinator_backup.json` bestand.", "title": "Upload een handmatige back-up" - }, - "user": { - "data": { - "path": "Serieel apparaatpad" - }, - "description": "Selecteer seri\u00eble poort voor Zigbee-radio", - "title": "ZHA" } } }, @@ -174,6 +152,7 @@ "data": { "uploaded_backup_file": "Een bestand uploaden" }, + "description": "Herstel je netwerkinstellingen van ge-upload backup-JSON-bestand. Je kunt deze dpwnloaden van een andere ZHA installatie via **Netwerkinstellingen**, of gebruik een Zigbee2MQTT `coordinator_backup.json` bestand.", "title": "Upload een handmatige back-up" } } diff --git a/homeassistant/components/zha/translations/nn.json b/homeassistant/components/zha/translations/nn.json index 9e9b677ddc1..65c3047fcc4 100644 --- a/homeassistant/components/zha/translations/nn.json +++ b/homeassistant/components/zha/translations/nn.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "port_config": { - "title": "Innstillinger" - }, - "user": { - "title": "ZHA" - } - } - }, "device_automation": { "action_type": { "squawk": "Squawk" diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 989409f7436..1434d243522 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -64,35 +64,12 @@ "description": "Sikkerhetskopien din har en annen IEEE-adresse enn radioen din. For at nettverket skal fungere ordentlig, b\u00f8r IEEE-adressen til radioen ogs\u00e5 endres. \n\n Dette er en permanent operasjon.", "title": "Overskriv radio IEEE-adresse" }, - "pick_radio": { - "data": { - "radio_type": "Radio type" - }, - "description": "Velg din type Zigbee-radio", - "title": "Radio type" - }, - "port_config": { - "data": { - "baudrate": "porthastighet", - "flow_control": "data flytkontroll", - "path": "Seriell enhetsbane" - }, - "description": "Angi portspesifikke innstillinger", - "title": "Innstillinger" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Last opp en fil" }, "description": "Gjenopprett nettverksinnstillingene fra en opplastet backup JSON-fil. Du kan laste ned en fra en annen ZHA-installasjon fra **Nettverksinnstillinger**, eller bruke en Zigbee2MQTT `coordinator_backup.json`-fil.", "title": "Last opp en manuell sikkerhetskopiering" - }, - "user": { - "data": { - "path": "Seriell enhetsbane" - }, - "description": "Velg seriell port for Zigbee radio", - "title": "" } } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index b2046848f0a..c3593b862ce 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -64,35 +64,12 @@ "description": "Twoja kopia zapasowa ma inny adres IEEE ni\u017c twoje radio. Aby sie\u0107 dzia\u0142a\u0142a prawid\u0142owo, nale\u017cy r\u00f3wnie\u017c zmieni\u0107 adres IEEE radia. \n\nTo jest trwa\u0142a operacja.", "title": "Nadpisanie adresu IEEE radia" }, - "pick_radio": { - "data": { - "radio_type": "Typ radia" - }, - "description": "Wyb\u00f3r typu radia Zigbee", - "title": "Typ radia" - }, - "port_config": { - "data": { - "baudrate": "pr\u0119dko\u015b\u0107 portu", - "flow_control": "kontrola przep\u0142ywu danych", - "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" - }, - "description": "Wprowadzanie ustawie\u0144 dla portu", - "title": "Ustawienia" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Prze\u015blij plik" }, "description": "Przywr\u00f3\u0107 ustawienia sieciowe z pliku kopii zapasowej JSON. Mo\u017cesz go pobra\u0107 z innej instalacji ZHA z **Ustawienia sieci** lub u\u017cy\u0107 pliku `coordinator_backup.json` z Zigbee2MQTT.", "title": "Przesy\u0142anie r\u0119cznej kopii zapasowej" - }, - "user": { - "data": { - "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" - }, - "description": "Wyb\u00f3r portu szeregowego dla radia Zigbee", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index ba0ac930f1e..0eac3642f58 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -64,35 +64,12 @@ "description": "Seu backup tem um endere\u00e7o IEEE diferente do seu r\u00e1dio. Para que sua rede funcione corretamente, o endere\u00e7o IEEE do seu r\u00e1dio tamb\u00e9m deve ser alterado. \n\n Esta \u00e9 uma opera\u00e7\u00e3o permanente.", "title": "Sobrescrever o endere\u00e7o IEEE do r\u00e1dio" }, - "pick_radio": { - "data": { - "radio_type": "Tipo de hub zigbee" - }, - "description": "Escolha o tipo de seu hub Zigbee", - "title": "Tipo de hub zigbee" - }, - "port_config": { - "data": { - "baudrate": "velocidade da porta", - "flow_control": "controle de fluxo de dados", - "path": "Caminho do dispositivo serial" - }, - "description": "Digite configura\u00e7\u00f5es espec\u00edficas da porta", - "title": "Configura\u00e7\u00f5es" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Carregar um arquivo" }, "description": "Restaure suas configura\u00e7\u00f5es de rede de um arquivo JSON de backup carregado. Voc\u00ea pode baixar um de uma instala\u00e7\u00e3o ZHA diferente em **Network Settings**, ou usar um arquivo Zigbee2MQTT `coordinator_backup.json`.", "title": "Carregar um backup manualmente" - }, - "user": { - "data": { - "path": "Caminho do dispositivo serial" - }, - "description": "Selecione a porta serial para o hub Zigbee", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 7fe13349c9f..c6207a73c61 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -64,9 +64,6 @@ }, "description": "Restaure suas configura\u00e7\u00f5es de rede de um arquivo JSON de backup carregado. Voc\u00ea pode baixar um de uma instala\u00e7\u00e3o ZHA diferente em **Network Settings**, ou usar um arquivo Zigbee2MQTT `coordinator_backup.json`.", "title": "Carregar um backup manual" - }, - "user": { - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index a8e58f3ecd9..bc935c9aa75 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -64,35 +64,12 @@ "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 IEEE-\u0430\u0434\u0440\u0435\u0441 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0441\u0435\u0439\u0447\u0430\u0441. \u0427\u0442\u043e\u0431\u044b \u0412\u0430\u0448\u0430 \u0441\u0435\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u0434\u043e\u043b\u0436\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, IEEE-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d. \n\n\u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0440\u0430\u0442\u0438\u043c\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u044f.", "title": "\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044c IEEE-\u0430\u0434\u0440\u0435\u0441\u0430 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" }, - "pick_radio": { - "data": { - "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f Zigbee", - "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" - }, - "port_config": { - "data": { - "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0430", - "flow_control": "\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0445", - "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, - "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0440\u0442\u0430", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b" }, "description": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0441\u0435\u0442\u0438 \u0438\u0437 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430 JSON. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0433\u043e \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 ZHA \u0438\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0430 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0442\u0438** \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b \u0438\u0437 Zigbee2MQTT `coordinator_backup.json`.", "title": "\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" - }, - "user": { - "data": { - "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440\u0430 \u0441\u0435\u0442\u0438 Zigbee", - "title": "Zigbee Home Automation" } } }, diff --git a/homeassistant/components/zha/translations/sk.json b/homeassistant/components/zha/translations/sk.json new file mode 100644 index 00000000000..3e8e32d1035 --- /dev/null +++ b/homeassistant/components/zha/translations/sk.json @@ -0,0 +1,75 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_backup_json": "Neplatn\u00e1 z\u00e1loha JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Vyberte si automatick\u00e9 z\u00e1lohovanie" + }, + "description": "Obnovte nastavenia siete z automatickej z\u00e1lohy", + "title": "Obnovenie automatickej z\u00e1lohy" + }, + "choose_formation_strategy": { + "menu_options": { + "choose_automatic_backup": "Obnovenie automatickej z\u00e1lohy", + "upload_manual_backup": "Nahratie manu\u00e1lnej z\u00e1lohy" + } + }, + "confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "confirm_hardware": { + "description": "Chcete nastavi\u0165 {name}?" + } + } + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Obe tla\u010didl\u00e1", + "button_1": "Prv\u00e9 tla\u010didlo", + "button_2": "Druh\u00e9 tla\u010didlo", + "button_3": "Tretie tla\u010didlo", + "button_4": "\u0160tvrt\u00e9 tla\u010didlo", + "button_5": "Piate tla\u010didlo", + "button_6": "\u0160ieste tla\u010didlo" + }, + "trigger_type": { + "device_offline": "Zariadenie je offline" + } + }, + "options": { + "abort": { + "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_backup_json": "Neplatn\u00e1 z\u00e1loha JSON" + }, + "flow_title": "{name}", + "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Vyberte si automatick\u00e9 z\u00e1lohovanie" + }, + "description": "Obnovte nastavenia siete z automatickej z\u00e1lohy", + "title": "Obnovenie automatickej z\u00e1lohy" + }, + "choose_formation_strategy": { + "menu_options": { + "choose_automatic_backup": "Obnovenie automatickej z\u00e1lohy", + "upload_manual_backup": "Nahratie manu\u00e1lnej z\u00e1lohy" + } + }, + "prompt_migrate_or_reconfigure": { + "title": "Migr\u00e1cia alebo zmena konfigur\u00e1cie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/sl.json b/homeassistant/components/zha/translations/sl.json index 54d87c087bb..d60f7de4dcf 100644 --- a/homeassistant/components/zha/translations/sl.json +++ b/homeassistant/components/zha/translations/sl.json @@ -5,11 +5,6 @@ }, "error": { "cannot_connect": "Ne morem se povezati napravo ZHA." - }, - "step": { - "user": { - "title": "ZHA" - } } }, "device_automation": { diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index a95dc2970fc..5fb36e9644b 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -64,35 +64,12 @@ "description": "Din s\u00e4kerhetskopia har en annan IEEE-adress \u00e4n din radio. F\u00f6r att ditt n\u00e4tverk ska fungera korrekt b\u00f6r IEEE-adressen f\u00f6r din radio ocks\u00e5 \u00e4ndras. \n\n Detta \u00e4r en permanent \u00e5tg\u00e4rd.", "title": "Skriv \u00f6ver Radio IEEE-adress" }, - "pick_radio": { - "data": { - "radio_type": "Radiotyp" - }, - "description": "V\u00e4lj en typ av Zigbee radio", - "title": "Radiotyp" - }, - "port_config": { - "data": { - "baudrate": "port hastighet", - "flow_control": "datafl\u00f6deskontroll", - "path": "Seriell enhetsv\u00e4g" - }, - "description": "Ange portspecifika inst\u00e4llningar", - "title": "Inst\u00e4llningar" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Ladda upp en fil" }, "description": "\u00c5terst\u00e4ll dina n\u00e4tverksinst\u00e4llningar fr\u00e5n en uppladdad backup-JSON-fil. Du kan ladda ner en fr\u00e5n en annan ZHA-installation fr\u00e5n **N\u00e4tverksinst\u00e4llningar**, eller anv\u00e4nda en Zigbee2MQTT `coordinator_backup.json`-fil.", "title": "Ladda upp en manuell s\u00e4kerhetskopia" - }, - "user": { - "data": { - "path": "Seriell enhetsv\u00e4g" - }, - "description": "V\u00e4lj serieport f\u00f6r Zigbee-radio", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index f309ecf92a0..e4409a7a1f0 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -64,35 +64,12 @@ "description": "Yedeklemenizin, telsizinizden farkl\u0131 bir IEEE adresi var. A\u011f\u0131n\u0131z\u0131n d\u00fczg\u00fcn \u00e7al\u0131\u015fmas\u0131 i\u00e7in telsizinizin IEEE adresinin de de\u011fi\u015ftirilmesi gerekir. \n\n Bu kal\u0131c\u0131 bir operasyondur.", "title": "Radyo IEEE Adresinin \u00dczerine Yaz" }, - "pick_radio": { - "data": { - "radio_type": "Radyo Tipi" - }, - "description": "Zigbee radyonuzun bir t\u00fcr\u00fcn\u00fc se\u00e7in", - "title": "Radyo Tipi" - }, - "port_config": { - "data": { - "baudrate": "ba\u011flant\u0131 noktas\u0131 h\u0131z\u0131", - "flow_control": "veri ak\u0131\u015f\u0131 denetimi", - "path": "Seri cihaz yolu" - }, - "description": "Ba\u011flant\u0131 noktas\u0131na \u00f6zel ayarlar\u0131 girin", - "title": "Ayarlar" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Bir dosya y\u00fckleyin" }, "description": "Y\u00fcklenen bir yedek JSON dosyas\u0131ndan a\u011f ayarlar\u0131n\u0131z\u0131 geri y\u00fckleyin. **A\u011f Ayarlar\u0131**'ndan farkl\u0131 bir ZHA kurulumundan bir tane indirebilir veya bir Zigbee2MQTT `coordinator_backup.json` dosyas\u0131 kullanabilirsiniz.", "title": "Manuel Yedekleme Y\u00fckleyin" - }, - "user": { - "data": { - "path": "Seri Cihaz Yolu" - }, - "description": "Zigbee radyo i\u00e7in seri ba\u011flant\u0131 noktas\u0131 se\u00e7in", - "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/uk.json b/homeassistant/components/zha/translations/uk.json index f7206911534..7e9fdbbca54 100644 --- a/homeassistant/components/zha/translations/uk.json +++ b/homeassistant/components/zha/translations/uk.json @@ -5,31 +5,6 @@ }, "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": { diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index c2b8d9f7cc2..f8db217decc 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -5,31 +5,6 @@ }, "error": { "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 ZHA \u8bbe\u5907\u3002" - }, - "step": { - "pick_radio": { - "data": { - "radio_type": "\u65e0\u7ebf\u7535\u7c7b\u578b" - }, - "description": "\u8bf7\u9009\u62e9 Zigbee \u65e0\u7ebf\u7535\u7c7b\u578b", - "title": "\u65e0\u7ebf\u7535\u7c7b\u578b" - }, - "port_config": { - "data": { - "baudrate": "\u6ce2\u7279\u7387", - "flow_control": "\u6570\u636e\u6d41\u63a7\u5236", - "path": "\u4e32\u884c\u8bbe\u5907\u8def\u5f84" - }, - "description": "\u8f93\u5165\u7aef\u53e3\u7684\u7279\u5b9a\u8bbe\u7f6e", - "title": "\u8bbe\u7f6e" - }, - "user": { - "data": { - "path": "\u4e32\u884c\u8bbe\u5907\u8def\u5f84" - }, - "description": "\u9009\u62e9 Zigbee \u7684\u4e32\u884c\u7aef\u53e3", - "title": "ZHA" - } } }, "device_automation": { diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 23d64c8cc42..78cd1dfdb66 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -64,35 +64,12 @@ "description": "\u5099\u4efd\u4e2d\u7684 IEEE \u4f4d\u5740\u8207\u73fe\u6709\u7121\u7dda\u96fb\u4e0d\u540c\u3002\u70ba\u4e86\u78ba\u8a8d\u7db2\u8def\u6b63\u5e38\u5de5\u4f5c\uff0c\u7121\u7dda\u96fb\u7684 IEEE \u4f4d\u5740\u5fc5\u9808\u9032\u884c\u8b8a\u66f4\u3002\n\n\u6b64\u70ba\u6c38\u4e45\u6027\u64cd\u4f5c\u3002.", "title": "\u8986\u5beb\u7121\u7dda\u96fb IEEE \u4f4d\u5740" }, - "pick_radio": { - "data": { - "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" - }, - "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u5225", - "title": "\u7121\u7dda\u96fb\u985e\u5225" - }, - "port_config": { - "data": { - "baudrate": "\u901a\u8a0a\u57e0\u901f\u5ea6", - "flow_control": "\u8cc7\u6599\u6d41\u91cf\u63a7\u5236", - "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" - }, - "description": "\u8f38\u5165\u901a\u8a0a\u57e0\u7279\u5b9a\u8a2d\u5b9a", - "title": "\u8a2d\u5b9a" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u4e0a\u50b3\u6a94\u6848" }, "description": "\u7531\u4e0a\u50b3\u7684\u5099\u4efd JSON \u6a94\u6848\u4e2d\u56de\u5fa9\u7db2\u8def\u8a2d\u5b9a\u3002\u53ef\u4ee5\u7531\u4e0d\u540c\u7684 ZHA \u5b89\u88dd\u4e2d\u7684 **\u7db2\u8def\u8a2d\u5b9a** \u9032\u884c\u4e0b\u8f09\u3001\u6216\u4f7f\u7528 Zigbee2MQTT \u4e2d\u7684 `coordinator_backup.json` \u6a94\u6848\u3002", "title": "\u4e0a\u50b3\u624b\u52d5\u5099\u4efd" - }, - "user": { - "data": { - "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" - }, - "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u5e8f\u5217\u57e0", - "title": "ZHA" } } }, diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index 48859cfb167..a0f789f1708 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -98,25 +98,24 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): """Initialize the device.""" self._mediabox = mediabox self._host = host - self._name = name - self._available = available - self._state = None + self._attr_name = name + self._attr_available = available def update(self) -> None: """Retrieve the state of the device.""" try: if self._mediabox.test_connection(): if self._mediabox.turned_on(): - if self._state != MediaPlayerState.PAUSED: - self._state = MediaPlayerState.PLAYING + if self.state != MediaPlayerState.PAUSED: + self._attr_state = MediaPlayerState.PLAYING else: - self._state = MediaPlayerState.OFF - self._available = True + self._attr_state = MediaPlayerState.OFF + self._attr_available = True else: - self._available = False + self._attr_available = False except OSError: _LOGGER.error("Couldn't fetch state from %s", self._host) - self._available = False + self._attr_available = False def send_keys(self, keys): """Send keys to the device and handle exceptions.""" @@ -126,22 +125,7 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): _LOGGER.error("Couldn't send keys to %s", self._host) @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def available(self): - """Return True if the device is available.""" - return self._available - - @property - def source_list(self): + def source_list(self) -> list[str]: """List of available sources (channels).""" return [ self._mediabox.channels()[c] @@ -159,30 +143,30 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): def media_play(self) -> None: """Send play command.""" self.send_keys(["PLAY"]) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def media_pause(self) -> None: """Send pause command.""" self.send_keys(["PAUSE"]) - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED def media_play_pause(self) -> None: """Simulate play pause media player.""" self.send_keys(["PAUSE"]) - if self._state == MediaPlayerState.PAUSED: - self._state = MediaPlayerState.PLAYING + if self.state == MediaPlayerState.PAUSED: + self._attr_state = MediaPlayerState.PLAYING else: - self._state = MediaPlayerState.PAUSED + self._attr_state = MediaPlayerState.PAUSED def media_next_track(self) -> None: """Channel up.""" self.send_keys(["CHAN_UP"]) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def media_previous_track(self) -> None: """Channel down.""" self.send_keys(["CHAN_DOWN"]) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING def select_source(self, source): """Select the channel.""" @@ -201,4 +185,4 @@ class ZiggoMediaboxXLDevice(MediaPlayerEntity): return self.send_keys([f"NUM_{digit}" for digit in str(digits)]) - self._state = MediaPlayerState.PLAYING + self._attr_state = MediaPlayerState.PLAYING diff --git a/homeassistant/components/zodiac/translations/sensor.sk.json b/homeassistant/components/zodiac/translations/sensor.sk.json new file mode 100644 index 00000000000..1d89a22507c --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.sk.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Vodn\u00e1r", + "aries": "Baran", + "cancer": "Rak", + "capricorn": "Kozoro\u017eec", + "gemini": "Bl\u00ed\u017eenci", + "leo": "Lev", + "libra": "V\u00e1hy", + "pisces": "Ryby", + "sagittarius": "Strelec", + "scorpio": "\u0160korpi\u00f3n", + "taurus": "B\u00fdk", + "virgo": "Panna" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/sk.json b/homeassistant/components/zone/translations/sk.json index 5272ec1315a..d1d6c76ca43 100644 --- a/homeassistant/components/zone/translations/sk.json +++ b/homeassistant/components/zone/translations/sk.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "name_exists": "N\u00e1zov u\u017e existuje" + }, "step": { "init": { "data": { + "icon": "Ikona", "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" } } } diff --git a/homeassistant/components/zoneminder/translations/sk.json b/homeassistant/components/zoneminder/translations/sk.json index 2c3ed1dd930..0a34af4a435 100644 --- a/homeassistant/components/zoneminder/translations/sk.json +++ b/homeassistant/components/zoneminder/translations/sk.json @@ -1,10 +1,26 @@ { "config": { "abort": { + "auth_fail": "U\u017eivate\u013esk\u00e9 meno alebo heslo je nespr\u00e1vne.", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "connection_error": "Nepodarilo sa pripoji\u0165 k serveru ZoneMinder.", "invalid_auth": "Neplatn\u00e9 overenie" }, "error": { + "auth_fail": "U\u017eivate\u013esk\u00e9 meno alebo heslo je nespr\u00e1vne.", + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e a port (napr. 10.10.0.4:8010)", + "password": "Heslo", + "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno", + "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index cab07f4287f..2d6e7350899 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -20,6 +20,7 @@ from zwave_js_server.model.notification import ( ) from zwave_js_server.model.value import Value, ValueNotification +from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -41,7 +42,7 @@ from homeassistant.helpers.issue_registry import ( ) from homeassistant.helpers.typing import UNDEFINED, ConfigType -from .addon import AddonError, AddonManager, AddonState, get_addon_manager +from .addon import get_addon_manager from .api import async_register_api from .const import ( ATTR_ACKNOWLEDGED_FRAMES, @@ -229,6 +230,7 @@ class DriverEvents: async def setup(self, driver: Driver) -> None: """Set up devices using the ready driver.""" self.driver = driver + controller = driver.controller # If opt in preference hasn't been specified yet, we do nothing, otherwise # we apply the preference @@ -243,7 +245,7 @@ class DriverEvents: ) known_devices = [ self.dev_reg.async_get_device({get_device_id(driver, node)}) - for node in driver.controller.nodes.values() + for node in controller.nodes.values() ] # Devices that are in the device registry that are not known by the controller can be removed @@ -251,17 +253,24 @@ class DriverEvents: if device not in known_devices: self.dev_reg.async_remove_device(device.id) - # run discovery on all ready nodes + # run discovery on controller node + c_node_id = controller.own_node_id + controller_node = controller.nodes.get(c_node_id) if c_node_id else None + if controller_node: + await self.controller_events.async_on_node_added(controller_node) + + # run discovery on all other ready nodes await asyncio.gather( *( self.controller_events.async_on_node_added(node) - for node in driver.controller.nodes.values() + for node in controller.nodes.values() + if controller_node is None or node != controller_node ) ) # listen for new nodes being added to the mesh self.config_entry.async_on_unload( - driver.controller.on( + controller.on( "node added", lambda event: self.hass.async_create_task( self.controller_events.async_on_node_added(event["node"]) @@ -271,9 +280,7 @@ class DriverEvents: # listen for nodes being removed from the mesh # NOTE: This will not remove nodes that were removed when HA was not running self.config_entry.async_on_unload( - driver.controller.on( - "node removed", self.controller_events.async_on_node_removed - ) + controller.on("node removed", self.controller_events.async_on_node_removed) ) async def async_setup_platform(self, platform: Platform) -> None: @@ -382,6 +389,16 @@ class ControllerEvents: device_id = get_device_id(driver, node) device_id_ext = get_device_id_ext(driver, node) device = self.dev_reg.async_get_device({device_id}) + via_device_id = None + controller = driver.controller + # Get the controller node device ID if this node is not the controller + if ( + controller.own_node_id is not None + and controller.own_node_id != node.node_id + ): + via_device_id = get_device_id( + driver, controller.nodes[controller.own_node_id] + ) # Replace the device if it can be determined that this node is not the # same product as it was previously. @@ -407,6 +424,7 @@ class ControllerEvents: model=node.device_config.label, manufacturer=node.device_config.manufacturer, suggested_area=node.location if node.location else UNDEFINED, + via_device=via_device_id, ) async_dispatcher_send(self.hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) @@ -716,7 +734,7 @@ class NodeEvents: raw_value = value_ = value.value if value.metadata.states: - value_ = value.metadata.states.get(str(value), value_) + value_ = value.metadata.states.get(str(value_), value_) self.hass.bus.async_fire( ZWAVE_JS_VALUE_UPDATED_EVENT, diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 3e27235ef84..f9adf9f19fb 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -1,39 +1,12 @@ """Provide add-on management.""" from __future__ import annotations -import asyncio -from collections.abc import Awaitable, Callable, Coroutine -from dataclasses import dataclass -from enum import Enum -from functools import partial, wraps -from typing import Any, TypeVar - -from typing_extensions import Concatenate, ParamSpec - -from homeassistant.components.hassio import ( - async_create_backup, - async_get_addon_discovery_info, - async_get_addon_info, - async_get_addon_store_info, - async_install_addon, - async_restart_addon, - async_set_addon_options, - async_start_addon, - async_stop_addon, - async_uninstall_addon, - async_update_addon, -) -from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.hassio import AddonManager from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.singleton import singleton from .const import ADDON_SLUG, DOMAIN, LOGGER -_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager") -_R = TypeVar("_R") -_P = ParamSpec("_P") - DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager" @@ -41,331 +14,4 @@ DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager" @callback def get_addon_manager(hass: HomeAssistant) -> AddonManager: """Get the add-on manager.""" - return AddonManager(hass, "Z-Wave JS", ADDON_SLUG) - - -def api_error( - error_message: str, -) -> Callable[ - [Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]], - Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]], -]: - """Handle HassioAPIError and raise a specific AddonError.""" - - def handle_hassio_api_error( - func: Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]] - ) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]: - """Handle a HassioAPIError.""" - - @wraps(func) - async def wrapper( - self: _AddonManagerT, *args: _P.args, **kwargs: _P.kwargs - ) -> _R: - """Wrap an add-on manager method.""" - try: - return_value = await func(self, *args, **kwargs) - except HassioAPIError as err: - raise AddonError( - f"{error_message.format(addon_name=self.addon_name)}: {err}" - ) from err - - return return_value - - return wrapper - - return handle_hassio_api_error - - -@dataclass -class AddonInfo: - """Represent the current add-on info state.""" - - options: dict[str, Any] - state: AddonState - update_available: bool - version: str | None - - -class AddonState(Enum): - """Represent the current state of the add-on.""" - - NOT_INSTALLED = "not_installed" - INSTALLING = "installing" - UPDATING = "updating" - NOT_RUNNING = "not_running" - RUNNING = "running" - - -class AddonManager: - """Manage the add-on. - - Methods may raise AddonError. - Only one instance of this class may exist per add-on - to keep track of running add-on tasks. - """ - - def __init__(self, hass: HomeAssistant, addon_name: str, addon_slug: str) -> None: - """Set up the add-on manager.""" - self.addon_name = addon_name - self.addon_slug = addon_slug - self._hass = hass - self._install_task: asyncio.Task | None = None - self._restart_task: asyncio.Task | None = None - self._start_task: asyncio.Task | None = None - self._update_task: asyncio.Task | None = None - - def task_in_progress(self) -> bool: - """Return True if any of the add-on tasks are in progress.""" - return any( - task and not task.done() - for task in ( - self._install_task, - self._start_task, - self._update_task, - ) - ) - - @api_error("Failed to get {addon_name} add-on discovery info") - async def async_get_addon_discovery_info(self) -> dict: - """Return add-on discovery info.""" - discovery_info = await async_get_addon_discovery_info( - self._hass, self.addon_slug - ) - - if not discovery_info: - raise AddonError(f"Failed to get {self.addon_name} add-on discovery info") - - discovery_info_config: dict = discovery_info["config"] - return discovery_info_config - - @api_error("Failed to get the {addon_name} add-on info") - async def async_get_addon_info(self) -> AddonInfo: - """Return and cache manager add-on info.""" - addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug) - LOGGER.debug("Add-on store info: %s", addon_store_info) - if not addon_store_info["installed"]: - return AddonInfo( - options={}, - state=AddonState.NOT_INSTALLED, - update_available=False, - version=None, - ) - - addon_info = await async_get_addon_info(self._hass, self.addon_slug) - addon_state = self.async_get_addon_state(addon_info) - return AddonInfo( - options=addon_info["options"], - state=addon_state, - update_available=addon_info["update_available"], - version=addon_info["version"], - ) - - @callback - def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: - """Return the current state of the managed add-on.""" - addon_state = AddonState.NOT_RUNNING - - if addon_info["state"] == "started": - addon_state = AddonState.RUNNING - if self._install_task and not self._install_task.done(): - addon_state = AddonState.INSTALLING - if self._update_task and not self._update_task.done(): - addon_state = AddonState.UPDATING - - return addon_state - - @api_error("Failed to set the {addon_name} add-on options") - async def async_set_addon_options(self, config: dict) -> None: - """Set manager add-on options.""" - options = {"options": config} - await async_set_addon_options(self._hass, self.addon_slug, options) - - @api_error("Failed to install the {addon_name} add-on") - async def async_install_addon(self) -> None: - """Install the managed add-on.""" - await async_install_addon(self._hass, self.addon_slug) - - @callback - def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that installs the managed add-on. - - Only schedule a new install task if the there's no running task. - """ - if not self._install_task or self._install_task.done(): - LOGGER.info( - "%s add-on is not installed. Installing add-on", self.addon_name - ) - self._install_task = self._async_schedule_addon_operation( - self.async_install_addon, catch_error=catch_error - ) - return self._install_task - - @callback - def async_schedule_install_setup_addon( - self, - addon_config: dict[str, Any], - catch_error: bool = False, - ) -> asyncio.Task: - """Schedule a task that installs and sets up the managed add-on. - - Only schedule a new install task if the there's no running task. - """ - if not self._install_task or self._install_task.done(): - LOGGER.info( - "%s add-on is not installed. Installing add-on", self.addon_name - ) - self._install_task = self._async_schedule_addon_operation( - self.async_install_addon, - partial( - self.async_configure_addon, - addon_config, - ), - self.async_start_addon, - catch_error=catch_error, - ) - return self._install_task - - @api_error("Failed to uninstall the {addon_name} add-on") - async def async_uninstall_addon(self) -> None: - """Uninstall the managed add-on.""" - await async_uninstall_addon(self._hass, self.addon_slug) - - @api_error("Failed to update the {addon_name} add-on") - async def async_update_addon(self) -> None: - """Update the managed add-on if needed.""" - addon_info = await self.async_get_addon_info() - - if addon_info.state is AddonState.NOT_INSTALLED: - raise AddonError(f"{self.addon_name} add-on is not installed") - - if not addon_info.update_available: - return - - await self.async_create_backup() - await async_update_addon(self._hass, self.addon_slug) - - @callback - def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that updates and sets up the managed add-on. - - Only schedule a new update task if the there's no running task. - """ - if not self._update_task or self._update_task.done(): - LOGGER.info("Trying to update the %s add-on", self.addon_name) - self._update_task = self._async_schedule_addon_operation( - self.async_update_addon, - catch_error=catch_error, - ) - return self._update_task - - @api_error("Failed to start the {addon_name} add-on") - async def async_start_addon(self) -> None: - """Start the managed add-on.""" - await async_start_addon(self._hass, self.addon_slug) - - @api_error("Failed to restart the {addon_name} add-on") - async def async_restart_addon(self) -> None: - """Restart the managed add-on.""" - await async_restart_addon(self._hass, self.addon_slug) - - @callback - def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that starts the managed add-on. - - Only schedule a new start task if the there's no running task. - """ - if not self._start_task or self._start_task.done(): - LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name) - self._start_task = self._async_schedule_addon_operation( - self.async_start_addon, catch_error=catch_error - ) - return self._start_task - - @callback - def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that restarts the managed add-on. - - Only schedule a new restart task if the there's no running task. - """ - if not self._restart_task or self._restart_task.done(): - LOGGER.info("Restarting %s add-on", self.addon_name) - self._restart_task = self._async_schedule_addon_operation( - self.async_restart_addon, catch_error=catch_error - ) - return self._restart_task - - @api_error("Failed to stop the {addon_name} add-on") - async def async_stop_addon(self) -> None: - """Stop the managed add-on.""" - await async_stop_addon(self._hass, self.addon_slug) - - async def async_configure_addon( - self, - addon_config: dict[str, Any], - ) -> None: - """Configure and start manager add-on.""" - addon_info = await self.async_get_addon_info() - - if addon_info.state is AddonState.NOT_INSTALLED: - raise AddonError(f"{self.addon_name} add-on is not installed") - - if addon_config != addon_info.options: - await self.async_set_addon_options(addon_config) - - @callback - def async_schedule_setup_addon( - self, - addon_config: dict[str, Any], - catch_error: bool = False, - ) -> asyncio.Task: - """Schedule a task that configures and starts the managed add-on. - - Only schedule a new setup task if there's no running task. - """ - if not self._start_task or self._start_task.done(): - LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name) - self._start_task = self._async_schedule_addon_operation( - partial( - self.async_configure_addon, - addon_config, - ), - self.async_start_addon, - catch_error=catch_error, - ) - return self._start_task - - @api_error("Failed to create a backup of the {addon_name} add-on.") - async def async_create_backup(self) -> None: - """Create a partial backup of the managed add-on.""" - addon_info = await self.async_get_addon_info() - name = f"addon_{self.addon_slug}_{addon_info.version}" - - LOGGER.debug("Creating backup: %s", name) - await async_create_backup( - self._hass, - {"name": name, "addons": [self.addon_slug]}, - partial=True, - ) - - @callback - def _async_schedule_addon_operation( - self, *funcs: Callable, catch_error: bool = False - ) -> asyncio.Task: - """Schedule an add-on task.""" - - async def addon_operation() -> None: - """Do the add-on operation and catch AddonError.""" - for func in funcs: - try: - await func() - except AddonError as err: - if not catch_error: - raise - LOGGER.error(err) - break - - return self._hass.async_create_task(addon_operation()) - - -class AddonError(HomeAssistantError): - """Represent an error with the managed add-on.""" + return AddonManager(hass, LOGGER, "Z-Wave JS", ADDON_SLUG) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 6cbb1ea3016..03f7fa94642 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -34,12 +34,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TEMPERATURE, - PRECISION_TENTHS, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -187,7 +182,6 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): check_all_endpoints=True, ) self._set_modes_and_presets() - self._attr_supported_features = 0 if self._current_mode and len(self._hvac_presets) > 1: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE # If any setpoint value exists, we can assume temperature @@ -260,8 +254,8 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): and self._unit_value.metadata.unit and "f" in self._unit_value.metadata.unit.lower() ): - return TEMP_FAHRENHEIT - return TEMP_CELSIUS + return UnitOfTemperature.FAHRENHEIT + return UnitOfTemperature.CELSIUS @property def hvac_mode(self) -> HVACMode: @@ -398,7 +392,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): def min_temp(self) -> float: """Return the minimum temperature.""" min_temp = DEFAULT_MIN_TEMP - base_unit = TEMP_CELSIUS + base_unit: str = UnitOfTemperature.CELSIUS try: temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.min: @@ -414,7 +408,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): def max_temp(self) -> float: """Return the maximum temperature.""" max_temp = DEFAULT_MAX_TEMP - base_unit = TEMP_CELSIUS + base_unit: str = UnitOfTemperature.CELSIUS try: temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.max: diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 0a084b3a309..11fd3da0e75 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -14,7 +14,14 @@ from zwave_js_server.version import VersionInfo, get_server_version from homeassistant import config_entries, exceptions from homeassistant.components import usb -from homeassistant.components.hassio import HassioServiceInfo, is_hassio +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + HassioServiceInfo, + is_hassio, +) from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback @@ -27,7 +34,7 @@ from homeassistant.data_entry_flow import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import disconnect_client -from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager +from .addon import get_addon_manager from .const import ( ADDON_SLUG, CONF_ADDON_DEVICE, diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 43b51048de4..4704718c804 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -129,7 +129,7 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): if self.info.primary_value.value is None: # guard missing value return None - return round((self.info.primary_value.value / 99) * 100) + return round((cast(int, self.info.primary_value.value) / 99) * 100) async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 004e4cc2aae..54dd17b7b83 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -150,6 +150,9 @@ async def async_get_actions( node = async_get_node_from_device_id(hass, device_id) + if node.client.driver and node.client.driver.controller.own_node_id == node.node_id: + return actions + base_action = { CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 25cce978df1..11c4fde3137 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import cast import voluptuous as vol +from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ConfigurationValueType from zwave_js_server.model.node import Node from zwave_js_server.model.value import ConfigurationValue @@ -12,7 +13,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from .const import DOMAIN +from .const import DATA_CLIENT, DOMAIN NODE_STATUSES = ["asleep", "awake", "dead", "alive"] @@ -66,4 +67,9 @@ def async_bypass_dynamic_config_validation(hass: HomeAssistant, device_id: str) ), None, ) - return not entry + if not entry: + return True + + # The driver may not be ready when the config entry is loaded. + client: ZwaveClient = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + return client.driver is None diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index c42b5af71c4..87967c21dd5 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -126,7 +126,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Z-Wave JS devices.""" - conditions = [] + conditions: list[dict] = [] base_condition = { CONF_CONDITION: "device", CONF_DEVICE_ID: device_id, @@ -134,6 +134,9 @@ async def async_get_conditions( } node = async_get_node_from_device_id(hass, device_id) + if node.client.driver and node.client.driver.controller.own_node_id == node.node_id: + return conditions + # Any value's value condition conditions.append({**base_condition, CONF_TYPE: VALUE_TYPE}) diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 76a7f134d17..348346680d7 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -248,9 +248,6 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Z-Wave JS devices.""" - dev_reg = device_registry.async_get(hass) - node = async_get_node_from_device_id(hass, device_id, dev_reg) - triggers: list[dict] = [] base_trigger = { CONF_PLATFORM: "device", @@ -258,6 +255,12 @@ async def async_get_triggers( CONF_DOMAIN: DOMAIN, } + dev_reg = device_registry.async_get(hass) + node = async_get_node_from_device_id(hass, device_id, dev_reg) + + if node.client.driver and node.client.driver.controller.own_node_id == node.node_id: + return triggers + # We can add a node status trigger if the node status sensor is enabled ent_reg = entity_registry.async_get(hass) entity_id = async_get_node_status_sensor_entity_id( diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 5b20572c2d8..0ee7c3d758e 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -96,35 +96,24 @@ from homeassistant.const import ( ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, FREQUENCY_KILOHERTZ, IRRADIATION_WATTS_PER_SQUARE_METER, - LENGTH_CENTIMETERS, - LENGTH_FEET, - LENGTH_METERS, LIGHT_LUX, - MASS_KILOGRAMS, - MASS_POUNDS, PERCENTAGE, - POWER_BTU_PER_HOUR, - POWER_WATT, - PRESSURE_INHG, - PRESSURE_MMHG, - PRESSURE_PSI, SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, TIME_SECONDS, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, - VOLUME_GALLONS, - VOLUME_LITERS, + UnitOfEnergy, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolume, UnitOfVolumetricFlux, ) @@ -173,21 +162,21 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = { METER_UNIT_MAP: dict[str, set[MeterScaleType]] = { ELECTRIC_CURRENT_AMPERE: METER_UNIT_AMPERE, - VOLUME_CUBIC_FEET: UNIT_CUBIC_FEET, - VOLUME_CUBIC_METERS: METER_UNIT_CUBIC_METER, - VOLUME_GALLONS: UNIT_US_GALLON, - ENERGY_KILO_WATT_HOUR: UNIT_KILOWATT_HOUR, + UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS: METER_UNIT_CUBIC_METER, + UnitOfVolume.GALLONS: UNIT_US_GALLON, + UnitOfEnergy.KILO_WATT_HOUR: UNIT_KILOWATT_HOUR, ELECTRIC_POTENTIAL_VOLT: METER_UNIT_VOLT, - POWER_WATT: METER_UNIT_WATT, + UnitOfPower.WATT: METER_UNIT_WATT, } MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { ELECTRIC_CURRENT_AMPERE: SENSOR_UNIT_AMPERE, - POWER_BTU_PER_HOUR: UNIT_BTU_H, - TEMP_CELSIUS: UNIT_CELSIUS, - LENGTH_CENTIMETERS: UNIT_CENTIMETER, + UnitOfPower.BTU_PER_HOUR: UNIT_BTU_H, + UnitOfTemperature.CELSIUS: UNIT_CELSIUS, + UnitOfLength.CENTIMETERS: UNIT_CENTIMETER, VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE, - VOLUME_CUBIC_METERS: SENSOR_UNIT_CUBIC_METER, + UnitOfVolume.CUBIC_METERS: SENSOR_UNIT_CUBIC_METER, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR, SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL, DEGREE: UNIT_DEGREES, @@ -195,31 +184,31 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { *UNIT_DENSITY, *UNIT_MICROGRAM_PER_CUBIC_METER, }, - TEMP_FAHRENHEIT: UNIT_FAHRENHEIT, - LENGTH_FEET: UNIT_FEET, - VOLUME_GALLONS: UNIT_GALLONS, + UnitOfTemperature.FAHRENHEIT: UNIT_FAHRENHEIT, + UnitOfLength.FEET: UNIT_FEET, + UnitOfVolume.GALLONS: UNIT_GALLONS, FREQUENCY_HERTZ: UNIT_HERTZ, - PRESSURE_INHG: UNIT_INCHES_OF_MERCURY, + UnitOfPressure.INHG: UNIT_INCHES_OF_MERCURY, UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR, - MASS_KILOGRAMS: UNIT_KILOGRAM, + UnitOfMass.KILOGRAMS: UNIT_KILOGRAM, FREQUENCY_KILOHERTZ: UNIT_KILOHERTZ, - VOLUME_LITERS: UNIT_LITER, + UnitOfVolume.LITERS: UNIT_LITER, LIGHT_LUX: UNIT_LUX, - LENGTH_METERS: UNIT_METER, + UnitOfLength.METERS: UNIT_METER, ELECTRIC_CURRENT_MILLIAMPERE: UNIT_MILLIAMPERE, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: UNIT_MILLIMETER_HOUR, ELECTRIC_POTENTIAL_MILLIVOLT: UNIT_MILLIVOLT, - SPEED_MILES_PER_HOUR: UNIT_MPH, - SPEED_METERS_PER_SECOND: UNIT_M_S, + UnitOfSpeed.MILES_PER_HOUR: UNIT_MPH, + UnitOfSpeed.METERS_PER_SECOND: UNIT_M_S, CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION, PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI}, - MASS_POUNDS: UNIT_POUNDS, - PRESSURE_PSI: UNIT_POUND_PER_SQUARE_INCH, + UnitOfMass.POUNDS: UNIT_POUNDS, + UnitOfPressure.PSI: UNIT_POUND_PER_SQUARE_INCH, SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL, TIME_SECONDS: UNIT_SECOND, - PRESSURE_MMHG: UNIT_SYSTOLIC, + UnitOfPressure.MMHG: UNIT_SYSTOLIC, ELECTRIC_POTENTIAL_VOLT: SENSOR_UNIT_VOLT, - POWER_WATT: SENSOR_UNIT_WATT, + UnitOfPower.WATT: SENSOR_UNIT_WATT, IRRADIATION_WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER, } diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 27f73353ca8..bf4942d76cc 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -250,9 +250,9 @@ class ValueMappingZwaveFan(ZwaveFan): return len(self.fan_value_mapping.speeds) @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" - flags: int = FanEntityFeature.SET_SPEED + flags = FanEntityFeature.SET_SPEED if self.has_fan_value_mapping and self.fan_value_mapping.presets: flags |= FanEntityFeature.PRESET_MODE @@ -387,7 +387,7 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): return list(self._fan_mode.metadata.states.values()) @property - def supported_features(self) -> int: + def supported_features(self) -> FanEntityFeature: """Flag supported features.""" return FanEntityFeature.PRESET_MODE diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 4c8fe2a3986..94d55dc1a2f 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -150,7 +150,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self._supported_color_modes.add(ColorMode.BRIGHTNESS) # Entity class attributes - self._attr_supported_features = 0 self.supports_brightness_transition = bool( self._target_brightness is not None and TRANSITION_DURATION_OPTION @@ -178,7 +177,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """ if self.info.primary_value.value is None: return None - return round((self.info.primary_value.value / 99) * 255) + return round((cast(int, self.info.primary_value.value) / 99) * 255) @property def color_mode(self) -> str | None: diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 38c1e8b181f..4dc86fed92c 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index 0ed3ce16d2f..73cd79beb62 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "not_zwave_js_addon": "\u041e\u0442\u043a\u0440\u0438\u0442\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u043d\u0435 \u0435 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u043d\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u043d\u0430 Z-Wave JS." }, "error": { diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 76bd5a35e96..ec325bae59a 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/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", - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "not_zwave_js_addon": "Dopln\u011bk Discovered nen\u00ed ofici\u00e1ln\u00edm dopl\u0148kem Z-Wave JS." }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index fb3c7e1a69d..3e1616c77c7 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -61,7 +61,7 @@ "description": "M\u00f6chtest du {name} mit dem Z-Wave JS Add-on einrichten?" }, "zeroconf_confirm": { - "description": "M\u00f6chtest du den Z-Wave JS-Server mit der Home-ID {home_id} , gefunden unter {url} , zu Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du den Z-Wave JS-Server mit der Home-ID {home_id}, gefunden unter {url}, zu Home Assistant hinzuf\u00fcgen?", "title": "Z-Wave JS-Server entdeckt" } } @@ -85,8 +85,8 @@ "event.notification.entry_control": "Benachrichtigung zur Zugangskontrolle gesendet", "event.notification.notification": "Benachrichtigung gesendet", "event.value_notification.basic": "Grundlegendes CC-Ereignis auf {subtype}", - "event.value_notification.central_scene": "Zentrale Szenenaktion auf {subtype}", - "event.value_notification.scene_activation": "Szenenaktivierung auf {subtype}", + "event.value_notification.central_scene": "Zentrale Szeneaktion auf {subtype}", + "event.value_notification.scene_activation": "Szeneaktivierung auf {subtype}", "state.node_status": "Knotenstatus ge\u00e4ndert", "zwave_js.value_updated.config_parameter": "Wert\u00e4nderung des Konfigurationsparameters {subtype}", "zwave_js.value_updated.value": "Wert\u00e4nderung bei einem Z-Wave JS Wert" diff --git a/homeassistant/components/zwave_js/translations/el.json b/homeassistant/components/zwave_js/translations/el.json index 9e671ee1aaa..d34b6762200 100644 --- a/homeassistant/components/zwave_js/translations/el.json +++ b/homeassistant/components/zwave_js/translations/el.json @@ -10,7 +10,8 @@ "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "discovery_requires_supervisor": "\u0397 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03c4\u03bf\u03bd \u03b5\u03c0\u03cc\u03c0\u03c4\u03b7.", - "not_zwave_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Z-Wave." + "not_zwave_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Z-Wave.", + "not_zwave_js_addon": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf \u03b5\u03c0\u03af\u03c3\u03b7\u03bc\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS." }, "error": { "addon_start_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7.", diff --git a/homeassistant/components/zwave_js/translations/is.json b/homeassistant/components/zwave_js/translations/is.json new file mode 100644 index 00000000000..8558e65ea2c --- /dev/null +++ b/homeassistant/components/zwave_js/translations/is.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condition_type": { + "node_status": "Sta\u00f0a hn\u00fats" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index d104f510eaf..d4377134977 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -62,7 +62,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere il server Z-Wave JS con l'ID casa {home_id} trovato in {url} a Home Assistant?", - "title": "Server JS Z-Wave rilevato" + "title": "Rilevato Server JS Z-Wave" } } }, diff --git a/homeassistant/components/zwave_js/translations/sk.json b/homeassistant/components/zwave_js/translations/sk.json index 833d18faafb..f6eb2028a39 100644 --- a/homeassistant/components/zwave_js/translations/sk.json +++ b/homeassistant/components/zwave_js/translations/sk.json @@ -2,12 +2,67 @@ "config": { "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_ws_url": "Neplatn\u00e1 adresa URL webov\u00e9ho soketu", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "configure_addon": { + "data": { + "usb_path": "Cesta k zariadeniu USB" + }, + "description": "Ak tieto polia zostan\u00fa pr\u00e1zdne, doplnok vygeneruje bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de." + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "Vyberte sp\u00f4sob pripojenia" + } + } + }, + "device_automation": { + "action_type": { + "ping": "Ping zariadenia" + }, + "trigger_type": { + "event.notification.notification": "Odosla\u0165 ozn\u00e1menie", + "event.value_notification.scene_activation": "Aktiv\u00e1cia sc\u00e9ny na {subtype}" } }, "options": { "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_ws_url": "Neplatn\u00e1 adresa URL webov\u00e9ho soketu", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "configure_addon": { + "data": { + "log_level": "\u00darove\u0148 denn\u00edka", + "usb_path": "Cesta k zariadeniu USB" + }, + "description": "Ak tieto polia zostan\u00fa pr\u00e1zdne, doplnok vygeneruje bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de." + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "Vyberte sp\u00f4sob pripojenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/triggers/trigger_helpers.py b/homeassistant/components/zwave_js/triggers/trigger_helpers.py index 706c4fc0aca..0fd9c3b4291 100644 --- a/homeassistant/components/zwave_js/triggers/trigger_helpers.py +++ b/homeassistant/components/zwave_js/triggers/trigger_helpers.py @@ -1,11 +1,13 @@ """Helpers for Z-Wave JS custom triggers.""" +from zwave_js_server.client import Client as ZwaveClient + from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType -from ..const import ATTR_CONFIG_ENTRY_ID, DOMAIN +from ..const import ATTR_CONFIG_ENTRY_ID, DATA_CLIENT, DOMAIN @callback @@ -19,9 +21,8 @@ def async_bypass_dynamic_config_validation( ent_reg = er.async_get(hass) trigger_devices = config.get(ATTR_DEVICE_ID, []) trigger_entities = config.get(ATTR_ENTITY_ID, []) - return any( - entry.state != ConfigEntryState.LOADED - and ( + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.state != ConfigEntryState.LOADED and ( entry.entry_id == config.get(ATTR_CONFIG_ENTRY_ID) or any( device.id in trigger_devices @@ -31,6 +32,12 @@ def async_bypass_dynamic_config_validation( entity.entity_id in trigger_entities for entity in er.async_entries_for_config_entry(ent_reg, entry.entry_id) ) - ) - for entry in hass.config_entries.async_entries(DOMAIN) - ) + ): + return True + + # The driver may not be ready when the config entry is loaded. + client: ZwaveClient = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + if client.driver is None: + return True + + return False diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index 10312f36dfc..ed3d538d052 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -8,6 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity @@ -23,6 +25,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: controller = hass.data[DOMAIN][entry.entry_id] = ZWaveMeController(hass, entry) if await controller.async_establish_connection(): hass.async_create_task(async_setup_platforms(hass, entry, controller)) + registry = device_registry.async_get(hass) + controller.remove_stale_devices(registry) return True raise ConfigEntryNotReady() @@ -62,24 +66,33 @@ class ZWaveMeController: def add_device(self, device: ZWaveMeData) -> None: """Send signal to create device.""" - if device.deviceType in ZWAVE_ME_PLATFORMS and self.platforms_inited: - if device.id in self.device_ids: - dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{device.id}", device) - else: - dispatcher_send( - self._hass, f"ZWAVE_ME_NEW_{device.deviceType.upper()}", device - ) - self.device_ids.add(device.id) + if device.id in self.device_ids: + dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{device.id}", device) + else: + dispatcher_send( + self._hass, f"ZWAVE_ME_NEW_{device.deviceType.upper()}", device + ) + self.device_ids.add(device.id) - def on_device_create(self, devices: list) -> None: + def on_device_create(self, devices: list[ZWaveMeData]) -> None: """Create multiple devices.""" for device in devices: - self.add_device(device) + if device.deviceType in ZWAVE_ME_PLATFORMS and self.platforms_inited: + self.add_device(device) def on_device_update(self, new_info: ZWaveMeData) -> None: """Send signal to update device.""" dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{new_info.id}", new_info) + def remove_stale_devices(self, registry: DeviceRegistry): + """Remove old-format devices in the registry.""" + for device_id in self.device_ids: + device = registry.async_get_device( + {(DOMAIN, f"{self.config.unique_id}-{device_id}")} + ) + if device is not None: + registry.async_remove_device(device.id) + async def async_setup_platforms( hass: HomeAssistant, entry: ConfigEntry, controller: ZWaveMeController @@ -113,7 +126,7 @@ class ZWaveMeEntity(Entity): def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, + identifiers={(DOMAIN, self.device.deviceIdentifier)}, name=self._attr_name, manufacturer=self.device.manufacturer, sw_version=self.device.firmware, diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 4ca933f43bc..9aeeb7b2a40 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave.Me", "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", - "requirements": ["zwave_me_ws==0.2.6", "url-normalize==1.4.3"], + "requirements": ["zwave_me_ws==0.3.0", "url-normalize==1.4.3"], "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, diff --git a/homeassistant/components/zwave_me/translations/de.json b/homeassistant/components/zwave_me/translations/de.json index 6e20c28ce07..af0249fe45b 100644 --- a/homeassistant/components/zwave_me/translations/de.json +++ b/homeassistant/components/zwave_me/translations/de.json @@ -13,7 +13,7 @@ "token": "API-Token", "url": "URL" }, - "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI \u2192 Men\u00fc \u2192 Einstellungen \u2192 Benutzer \u2192 Administrator \u2192 API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." + "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way Servers ein. Um das Token zu erhalten, gehe zur Z-Way Benutzeroberfl\u00e4che Smart Home UI \u2192 Men\u00fc \u2192 Einstellungen \u2192 Benutzer \u2192 Administrator \u2192 API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." } } } diff --git a/homeassistant/components/zwave_me/translations/sk.json b/homeassistant/components/zwave_me/translations/sk.json new file mode 100644 index 00000000000..badd53ca8a7 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "token": "API token", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/config.py b/homeassistant/config.py index e56dff4e491..e203c45f795 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Callable, Sequence +from contextlib import suppress import logging import os from pathlib import Path @@ -26,6 +27,7 @@ from .const import ( CONF_ALLOWLIST_EXTERNAL_URLS, CONF_AUTH_MFA_MODULES, CONF_AUTH_PROVIDERS, + CONF_COUNTRY, CONF_CURRENCY, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, @@ -34,6 +36,7 @@ from .const import ( CONF_EXTERNAL_URL, CONF_ID, CONF_INTERNAL_URL, + CONF_LANGUAGE, CONF_LATITUDE, CONF_LEGACY_TEMPLATES, CONF_LONGITUDE, @@ -49,10 +52,12 @@ from .const import ( ) from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback from .exceptions import HomeAssistantError +from .generated.currencies import HISTORIC_CURRENCIES from .helpers import ( config_per_platform, config_validation as cv, extract_domain_configs, + issue_registry as ir, ) from .helpers.entity_values import EntityValues from .helpers.typing import ConfigType @@ -199,6 +204,50 @@ CUSTOMIZE_CONFIG_SCHEMA = vol.Schema( } ) + +def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None: + if currency not in HISTORIC_CURRENCIES: + ir.async_delete_issue(hass, "homeassistant", "historic_currency") + return + + ir.async_create_issue( + hass, + "homeassistant", + "historic_currency", + is_fixable=False, + learn_more_url="homeassistant://config/general", + severity=ir.IssueSeverity.WARNING, + translation_key="historic_currency", + translation_placeholders={"currency": currency}, + ) + + +def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None: + if country is not None: + ir.async_delete_issue(hass, "homeassistant", "country_not_configured") + return + + ir.async_create_issue( + hass, + "homeassistant", + "country_not_configured", + is_fixable=False, + learn_more_url="homeassistant://config/general", + severity=ir.IssueSeverity.WARNING, + translation_key="country_not_configured", + ) + + +def _validate_currency(data: Any) -> Any: + try: + return cv.currency(data) + except vol.InInvalid: + with suppress(vol.InInvalid): + currency = cv.historic_currency(data) + return currency + raise + + CORE_CONFIG_SCHEMA = vol.All( CUSTOMIZE_CONFIG_SCHEMA.extend( { @@ -250,10 +299,12 @@ CORE_CONFIG_SCHEMA = vol.All( ], _no_duplicate_auth_mfa_module, ), - # pylint: disable=no-value-for-parameter + # pylint: disable-next=no-value-for-parameter vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, - vol.Optional(CONF_CURRENCY): cv.currency, + vol.Optional(CONF_CURRENCY): _validate_currency, + vol.Optional(CONF_COUNTRY): cv.country, + vol.Optional(CONF_LANGUAGE): cv.language, } ), _filter_bad_internal_external_urls, @@ -533,6 +584,8 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non CONF_EXTERNAL_URL, CONF_INTERNAL_URL, CONF_CURRENCY, + CONF_COUNTRY, + CONF_LANGUAGE, ) ): hac.config_source = ConfigSource.YAML @@ -547,10 +600,15 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non (CONF_MEDIA_DIRS, "media_dirs"), (CONF_LEGACY_TEMPLATES, "legacy_templates"), (CONF_CURRENCY, "currency"), + (CONF_COUNTRY, "country"), + (CONF_LANGUAGE, "language"), ): if key in config: setattr(hac, attr, config[key]) + _raise_issue_if_historic_currency(hass, hass.config.currency) + _raise_issue_if_no_country(hass, hass.config.country) + if CONF_TIME_ZONE in config: hac.set_time_zone(config[CONF_TIME_ZONE]) @@ -705,7 +763,7 @@ async def merge_packages_config( continue # If component name is given with a trailing description, remove it # when looking for component - domain = comp_name.split(" ")[0] + domain = comp_name.partition(" ")[0] try: integration = await async_get_integration_with_requirements( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bd985517ca7..4830ec602a7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,8 +3,9 @@ from __future__ import annotations import asyncio from collections import ChainMap -from collections.abc import Callable, Coroutine, Iterable, Mapping +from collections.abc import Callable, Coroutine, Generator, Iterable, Mapping from contextvars import ContextVar +from copy import deepcopy from enum import Enum import functools import logging @@ -17,13 +18,19 @@ from .backports.enum import StrEnum from .components import persistent_notification from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform from .core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback -from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError +from .data_entry_flow import FlowResult +from .exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, + HomeAssistantError, +) from .helpers import device_registry, entity_registry, storage from .helpers.dispatcher import async_dispatcher_send from .helpers.event import async_call_later from .helpers.frame import report from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType -from .setup import async_process_deps_reqs, async_setup_component +from .setup import DATA_SETUP_DONE, async_process_deps_reqs, async_setup_component from .util import uuid as uuid_util from .util.decorator import Registry @@ -369,6 +376,16 @@ class ConfigEntry: "%s.async_setup_entry did not return boolean", integration.domain ) result = False + except ConfigEntryError as ex: + error_reason = str(ex) or "Unknown fatal config entry error" + _LOGGER.exception( + "Error setting up entry %s for %s: %s", + self.title, + self.domain, + error_reason, + ) + await self._async_process_on_unload() + result = False except ConfigEntryAuthFailed as ex: message = str(ex) auth_base_message = "could not authenticate" @@ -660,12 +677,7 @@ class ConfigEntry: data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" - if any( - flow - for flow in hass.config_entries.flow.async_progress_by_handler(self.domain) - if flow["context"].get("source") == SOURCE_REAUTH - and flow["context"].get("entry_id") == self.entry_id - ): + if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})): # Reauth flow already in progress for this entry return @@ -683,6 +695,18 @@ class ConfigEntry: ) ) + @callback + def async_get_active_flows( + self, hass: HomeAssistant, sources: set[str] + ) -> Generator[FlowResult, None, None]: + """Get any active flows of certain sources for this entry.""" + return ( + flow + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain) + if flow["context"].get("source") in sources + and flow["context"].get("entry_id") == self.entry_id + ) + @callback def async_create_task( self, hass: HomeAssistant, target: Coroutine[Any, Any, _R] @@ -1274,6 +1298,22 @@ class ConfigEntries: """Return data to save.""" return {"entries": [entry.as_dict() for entry in self._entries.values()]} + async def async_wait_component(self, entry: ConfigEntry) -> bool: + """Wait for an entry's component to load and return if the entry is loaded. + + This is primarily intended for existing config entries which are loaded at + startup, awaiting this function will block until the component and all its + config entries are loaded. + Config entries which are created after Home Assistant is started can't be waited + for, the function will just return if the config entry is loaded or not. + """ + if setup_event := self.hass.data.get(DATA_SETUP_DONE, {}).get(entry.domain): + await setup_event.wait() + # The component was not loaded. + if entry.domain not in self.hass.config.components: + return False + return entry.state == ConfigEntryState.LOADED + async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]: """Migrate the pre-0.73 config format to the latest version.""" @@ -1648,11 +1688,30 @@ class OptionsFlowManager(data_entry_flow.FlowManager): class OptionsFlow(data_entry_flow.FlowHandler): - """Base class for config option flows.""" + """Base class for config options flows.""" handler: str +class OptionsFlowWithConfigEntry(OptionsFlow): + """Base class for options flows with config entry and options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self._config_entry = config_entry + self._options = deepcopy(dict(config_entry.options)) + + @property + def config_entry(self) -> ConfigEntry: + """Return the config entry.""" + return self._config_entry + + @property + def options(self) -> dict[str, Any]: + """Return a mutable copy of the config entry options.""" + return self._options + + class EntityRegistryDisabledHandler: """Handler to handle when entities related to config entries updating disabled_by.""" @@ -1719,7 +1778,7 @@ class EntityRegistryDisabledHandler: _LOGGER.info( "Reloading configuration entries because disabled_by changed in entity registry: %s", - ", ".join(self.changed), + ", ".join(to_reload), ) await asyncio.gather( diff --git a/homeassistant/const.py b/homeassistant/const.py index 4042ccf0620..70b1f7799e6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,14 +7,14 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "5" +MINOR_VERSION: Final = 12 +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) -REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) +REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2023.2" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" @@ -49,6 +49,7 @@ class Platform(StrEnum): SIREN = "siren" STT = "stt" SWITCH = "switch" + TEXT = "text" TTS = "tts" VACUUM = "vacuum" UPDATE = "update" @@ -120,6 +121,7 @@ CONF_CONDITIONS: Final = "conditions" CONF_CONTINUE_ON_ERROR: Final = "continue_on_error" CONF_CONTINUE_ON_TIMEOUT: Final = "continue_on_timeout" CONF_COUNT: Final = "count" +CONF_COUNTRY: Final = "country" CONF_COVERS: Final = "covers" CONF_CURRENCY: Final = "currency" CONF_CUSTOMIZE: Final = "customize" @@ -174,6 +176,7 @@ CONF_IF: Final = "if" CONF_INCLUDE: Final = "include" CONF_INTERNAL_URL: Final = "internal_url" CONF_IP_ADDRESS: Final = "ip_address" +CONF_LANGUAGE: Final = "language" CONF_LATITUDE: Final = "latitude" CONF_LEGACY_TEMPLATES: Final = "legacy_templates" CONF_LIGHTS: Final = "lights" @@ -743,15 +746,27 @@ class UnitOfVolumetricFlux(StrEnum): """Derived from mm³/(mm².h)""" -# Precipitation units -# The derivation of these units is a volume of rain amassing in a container -# with constant cross section -PRECIPITATION_INCHES: Final = "in" -PRECIPITATION_MILLIMETERS: Final = "mm" +class UnitOfPrecipitationDepth(StrEnum): + """Precipitation depth. + The derivation of these units is a volume of rain amassing in a container + with constant cross section + """ + + INCHES = "in" + """Derived from in³/in²""" + + MILLIMETERS = "mm" + """Derived from mm³/mm²""" + + +# Precipitation units +PRECIPITATION_INCHES: Final = "in" +"""Deprecated: please use UnitOfPrecipitationDepth.INCHES""" +PRECIPITATION_MILLIMETERS: Final = "mm" +"""Deprecated: please use UnitOfPrecipitationDepth.MILLIMETERS""" PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" """Deprecated: please use UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR""" - PRECIPITATION_INCHES_PER_HOUR: Final = "in/h" """Deprecated: please use UnitOfVolumetricFlux.INCHES_PER_HOUR""" diff --git a/homeassistant/core.py b/homeassistant/core.py index 8f9287aedac..82ee5216f4f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -15,6 +15,7 @@ from collections.abc import ( Iterable, Mapping, ) +from contextlib import suppress from contextvars import ContextVar import datetime import enum @@ -113,7 +114,7 @@ CALLBACK_TYPE = Callable[[], None] # pylint: disable=invalid-name CORE_STORAGE_KEY = "core.config" CORE_STORAGE_VERSION = 1 -CORE_STORAGE_MINOR_VERSION = 2 +CORE_STORAGE_MINOR_VERSION = 3 DOMAIN = "homeassistant" @@ -1807,12 +1808,17 @@ class Config: self.internal_url: str | None = None self.external_url: str | None = None self.currency: str = "EUR" + self.country: str | None = None + self.language: str = "en" self.config_source: ConfigSource = ConfigSource.DEFAULT # If True, pip install is skipped for requirements on startup self.skip_pip: bool = False + # List of packages to skip when installing requirements on startup + self.skip_pip_packages: list[str] = [] + # List of loaded components self.components: set[str] = set() @@ -1913,6 +1919,8 @@ class Config: "external_url": self.external_url, "internal_url": self.internal_url, "currency": self.currency, + "country": self.country, + "language": self.language, } def set_time_zone(self, time_zone_str: str) -> None: @@ -1938,6 +1946,8 @@ class Config: external_url: str | dict[Any, Any] | None = _UNDEF, internal_url: str | dict[Any, Any] | None = _UNDEF, currency: str | None = None, + country: str | dict[Any, Any] | None = _UNDEF, + language: str | None = None, ) -> None: """Update the configuration from a dictionary.""" self.config_source = source @@ -1962,13 +1972,26 @@ class Config: self.internal_url = cast(Optional[str], internal_url) if currency is not None: self.currency = currency + if country is not _UNDEF: + self.country = cast(Optional[str], country) + if language is not None: + self.language = language async def async_update(self, **kwargs: Any) -> None: """Update the configuration from a dictionary.""" + # pylint: disable-next=import-outside-toplevel + from .config import ( + _raise_issue_if_historic_currency, + _raise_issue_if_no_country, + ) + self._update(source=ConfigSource.STORAGE, **kwargs) await self._async_store() self.hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, kwargs) + _raise_issue_if_historic_currency(self.hass, self.currency) + _raise_issue_if_no_country(self.hass, self.country) + async def async_load(self) -> None: """Load [homeassistant] core config.""" if not (data := await self._store.async_load()): @@ -1999,6 +2022,8 @@ class Config: external_url=data.get("external_url", _UNDEF), internal_url=data.get("internal_url", _UNDEF), currency=data.get("currency"), + country=data.get("country"), + language=data.get("language"), ) async def _async_store(self) -> None: @@ -2015,6 +2040,8 @@ class Config: "external_url": self.external_url, "internal_url": self.internal_url, "currency": self.currency, + "country": self.country, + "language": self.language, } await self._store.async_save(data) @@ -2053,6 +2080,34 @@ class Config: data["unit_system_v2"] = self._original_unit_system if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL: data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY + if old_major_version == 1 and old_minor_version < 3: + # In 1.3, we add the key "language", initialize it from the owner account + data["language"] = "en" + try: + owner = await self.hass.auth.async_get_owner() + if owner is not None: + # pylint: disable-next=import-outside-toplevel + from .components.frontend import storage as frontend_store + + # pylint: disable-next=import-outside-toplevel + from .helpers import config_validation as cv + + _, owner_data = await frontend_store.async_user_store( + self.hass, owner.id + ) + + if ( + "language" in owner_data + and "language" in owner_data["language"] + ): + with suppress(vol.InInvalid): + data["language"] = cv.language( + owner_data["language"]["language"] + ) + # pylint: disable-next=broad-except + except Exception: + _LOGGER.exception("Unexpected error during core config migration") + if old_major_version > 1: raise NotImplementedError return data diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 629258e01d1..866f1a5db2f 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import abc import asyncio from collections.abc import Iterable, Mapping +import copy from dataclasses import dataclass import logging from types import MappingProxyType @@ -443,6 +444,34 @@ class FlowHandler: """If we should show advanced options.""" return self.context.get("show_advanced_options", False) + def add_suggested_values_to_schema( + self, data_schema: vol.Schema, suggested_values: Mapping[str, Any] + ) -> vol.Schema: + """Make a copy of the schema, populated with suggested values. + + For each schema marker matching items in `suggested_values`, + the `suggested_value` will be set. The existing `suggested_value` will + be left untouched if there is no matching item. + """ + schema = {} + for key, val in data_schema.schema.items(): + if isinstance(key, vol.Marker): + # Exclude advanced field + if ( + key.description + and key.description.get("advanced") + and not self.show_advanced_options + ): + continue + + new_key = key + if key in suggested_values and isinstance(key, vol.Marker): + # Copy the marker to not modify the flow schema + new_key = copy.copy(key) + new_key.description = {"suggested_value": suggested_values[key]} + schema[new_key] = val + return vol.Schema(schema) + @callback def async_show_form( self, diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 052d3de4768..77ac2938cf2 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -25,9 +25,12 @@ class NoEntitySpecifiedError(HomeAssistantError): class TemplateError(HomeAssistantError): """Error during template rendering.""" - def __init__(self, exception: Exception) -> None: + def __init__(self, exception: Exception | str) -> None: """Init the error.""" - super().__init__(f"{exception.__class__.__name__}: {exception}") + if isinstance(exception, str): + super().__init__(exception) + else: + super().__init__(f"{exception.__class__.__name__}: {exception}") @attr.s @@ -111,6 +114,10 @@ class PlatformNotReady(IntegrationError): """Error to indicate that platform is not ready.""" +class ConfigEntryError(IntegrationError): + """Error to indicate that config entry setup has failed.""" + + class ConfigEntryNotReady(IntegrationError): """Error to indicate that config entry is not ready.""" diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index d2e16f2b914..31e73418c5e 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -1,4 +1,4 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index c4dd22cef17..922e754e84a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -1,7 +1,8 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ + from __future__ import annotations BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ @@ -10,24 +11,40 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "manufacturer_id": 820, }, { - "domain": "bluemaestro", - "manufacturer_id": 307, "connectable": False, + "domain": "aranet", + "manufacturer_id": 1794, + "service_uuid": "f0cd1400-95da-4f4b-9ac8-aa55d312af0c", }, { - "domain": "bthome", "connectable": False, + "domain": "aranet", + "manufacturer_id": 1794, + "service_uuid": "0000fce0-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "bluemaestro", + "manufacturer_id": 307, + }, + { + "connectable": False, + "domain": "bthome", "service_data_uuid": "0000181c-0000-1000-8000-00805f9b34fb", }, { - "domain": "bthome", "connectable": False, + "domain": "bthome", "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb", }, { - "domain": "fjaraskupan", "connectable": False, - "manufacturer_id": 20296, + "domain": "bthome", + "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "fjaraskupan", "manufacturer_data_start": [ 79, 68, @@ -36,143 +53,144 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ 65, 82, ], + "manufacturer_id": 20296, }, { + "connectable": False, "domain": "govee_ble", "local_name": "Govee*", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "local_name": "GVH5*", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "local_name": "B5178*", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 6966, "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 63391, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 26589, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 57391, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 53579, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 43682, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 59970, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 63585, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 10032, "service_uuid": "00008251-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 19506, "service_uuid": "00001801-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { "domain": "homekit_controller", - "manufacturer_id": 76, "manufacturer_data_start": [ 6, ], + "manufacturer_id": 76, }, { "domain": "ibeacon", - "manufacturer_id": 76, "manufacturer_data_start": [ 2, 21, ], + "manufacturer_id": 76, }, { + "connectable": False, "domain": "inkbird", "local_name": "sps", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "Inkbird*", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "iBBQ*", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "xBBQ*", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "tps", - "connectable": False, }, { - "domain": "kegtron", "connectable": False, + "domain": "kegtron", "manufacturer_id": 65535, }, { @@ -223,55 +241,71 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "manufacturer_id": 13, }, { + "connectable": False, "domain": "moat", "local_name": "Moat_S*", - "connectable": False, }, { "domain": "oralb", "manufacturer_id": 220, }, { + "connectable": False, "domain": "qingping", "local_name": "Qingping*", - "connectable": False, }, { + "connectable": False, "domain": "qingping", "local_name": "Lee Guitars*", - "connectable": False, }, { + "connectable": False, "domain": "qingping", "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "domain": "ruuvitag_ble", + "manufacturer_id": 1177, + }, + { + "domain": "ruuvitag_ble", + "local_name": "Ruuvi *", + }, + { + "domain": "sensirion_ble", + "manufacturer_id": 1749, + }, + { + "domain": "sensirion_ble", + "local_name": "MyCO2*", + }, + { + "connectable": False, "domain": "sensorpro", - "manufacturer_id": 43605, "manufacturer_data_start": [ 1, 1, 164, 193, ], - "connectable": False, + "manufacturer_id": 43605, }, { + "connectable": False, "domain": "sensorpro", - "manufacturer_id": 43605, "manufacturer_data_start": [ 1, 5, 164, 193, ], - "connectable": False, + "manufacturer_id": 43605, }, { + "connectable": False, "domain": "sensorpush", "local_name": "SensorPush*", - "connectable": False, }, { "domain": "snooz", @@ -282,65 +316,73 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "729f0608-496a-47fe-a124-3a62aaa3fbc0", }, { + "connectable": False, "domain": "switchbot", "service_data_uuid": "00000d00-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "switchbot", "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", - "connectable": False, }, { + "connectable": False, "domain": "thermobeacon", - "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_data_start": [ + 0, + ], "manufacturer_id": 16, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "thermobeacon", "manufacturer_data_start": [ 0, ], - "connectable": False, - }, - { - "domain": "thermobeacon", - "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", "manufacturer_id": 17, - "manufacturer_data_start": [ - 0, - ], - "connectable": False, - }, - { - "domain": "thermobeacon", "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", - "manufacturer_id": 21, + }, + { + "connectable": False, + "domain": "thermobeacon", "manufacturer_data_start": [ 0, ], - "connectable": False, + "manufacturer_id": 21, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", }, { + "connectable": False, + "domain": "thermobeacon", + "manufacturer_data_start": [ + 0, + ], + "manufacturer_id": 24, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, "domain": "thermobeacon", "local_name": "ThermoBeacon", - "connectable": False, }, { + "connectable": False, "domain": "thermopro", "local_name": "TP35*", - "connectable": False, }, { + "connectable": False, "domain": "thermopro", "local_name": "TP39*", - "connectable": False, }, { "domain": "tilt_ble", - "manufacturer_id": 76, "manufacturer_data_start": [ 2, 21, @@ -348,10 +390,16 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ 149, 187, ], + "manufacturer_id": 76, }, { - "domain": "xiaomi_ble", "connectable": False, + "domain": "xiaomi_ble", + "service_data_uuid": "0000fd50-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "xiaomi_ble", "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb", }, { diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 772068401a5..5875c9021f6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -1,9 +1,19 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ FLOWS = { + "helper": [ + "derivative", + "group", + "integration", + "min_max", + "switch_as_x", + "threshold", + "tod", + "utility_meter", + ], "integration": [ "abode", "accuweather", @@ -15,6 +25,7 @@ FLOWS = { "agent_dvr", "airly", "airnow", + "airq", "airthings", "airthings_ble", "airtouch4", @@ -31,6 +42,7 @@ FLOWS = { "anthemav", "apcupsd", "apple_tv", + "aranet", "arcam_fmj", "aseko_pool_live", "asuswrt", @@ -150,7 +162,6 @@ FLOWS = { "growatt_server", "guardian", "habitica", - "hangouts", "harmony", "heos", "here_travel_time", @@ -159,6 +170,7 @@ FLOWS = { "hlk_sw16", "home_connect", "home_plus_control", + "homeassistant_sky_connect", "homekit", "homekit_controller", "homematicip_cloud", @@ -213,6 +225,8 @@ FLOWS = { "lifx", "litejet", "litterrobot", + "livisi", + "local_calendar", "local_ip", "locative", "logi_circle", @@ -221,6 +235,7 @@ FLOWS = { "lutron_caseta", "lyric", "mailgun", + "matter", "mazda", "meater", "melcloud", @@ -303,6 +318,7 @@ FLOWS = { "prusalink", "ps4", "pure_energie", + "pushbullet", "pushover", "pvoutput", "pvpc_hourly_pricing", @@ -329,13 +345,16 @@ FLOWS = { "rpi_power", "rtsp_to_webrtc", "ruckus_unleashed", + "ruuvitag_ble", "sabnzbd", "samsungtv", + "scrape", "screenlogic", "season", "sense", "senseme", "sensibo", + "sensirion_ble", "sensorpro", "sensorpush", "sentry", @@ -467,14 +486,4 @@ FLOWS = { "zwave_js", "zwave_me", ], - "helper": [ - "derivative", - "group", - "integration", - "min_max", - "switch_as_x", - "threshold", - "tod", - "utility_meter", - ], } diff --git a/homeassistant/generated/countries.py b/homeassistant/generated/countries.py new file mode 100644 index 00000000000..76482a524de --- /dev/null +++ b/homeassistant/generated/countries.py @@ -0,0 +1,260 @@ +"""This file is automatically generated. + +To update, run python3 -m script.countries + +The values are directly corresponding to the ISO 3166 standard. If you need changes +to the political situation in the world, please contact the ISO 3166 working group. + +""" + +COUNTRIES = { + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BL", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CU", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IR", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KP", + "KR", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MF", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SD", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "SS", + "ST", + "SV", + "SX", + "SY", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW", +} diff --git a/homeassistant/generated/currencies.py b/homeassistant/generated/currencies.py new file mode 100644 index 00000000000..546bc125a01 --- /dev/null +++ b/homeassistant/generated/currencies.py @@ -0,0 +1,290 @@ +"""This file is automatically generated. + +To update, run python3 -m script.currencies +""" + +ACTIVE_CURRENCIES = { + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLE", + "SLL", + "SOS", + "SRD", + "SSP", + "STN", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VED", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW", + "ZWL", +} + +HISTORIC_CURRENCIES = { + "ADP", + "AFA", + "ALK", + "AOK", + "AON", + "AOR", + "ARA", + "ARP", + "ARY", + "ATS", + "AYM", + "AZM", + "BAD", + "BEC", + "BEF", + "BEL", + "BGJ", + "BGK", + "BGL", + "BOP", + "BRB", + "BRC", + "BRE", + "BRN", + "BRR", + "BUK", + "BYB", + "BYR", + "CHC", + "CSD", + "CSJ", + "CSK", + "CYP", + "DDM", + "DEM", + "ECS", + "ECV", + "EEK", + "ESA", + "ESB", + "ESP", + "FIM", + "FRF", + "GEK", + "GHC", + "GHP", + "GNE", + "GNS", + "GQE", + "GRD", + "GWE", + "GWP", + "HRD", + "IEP", + "ILP", + "ILR", + "ISJ", + "ITL", + "LAJ", + "LSM", + "LTL", + "LTT", + "LUC", + "LUF", + "LUL", + "LVL", + "LVR", + "MGF", + "MLF", + "MRO", + "MTL", + "MTP", + "MVQ", + "MXP", + "MZE", + "MZM", + "NIC", + "NLG", + "PEH", + "PEI", + "PES", + "PLZ", + "PTE", + "RHD", + "ROK", + "ROL", + "RUR", + "SDD", + "SDP", + "SIT", + "SKK", + "SRG", + "STD", + "SUR", + "TJR", + "TMM", + "TPE", + "TRL", + "UAK", + "UGS", + "UGW", + "USS", + "UYN", + "UYP", + "VEB", + "VEF", + "VNC", + "XEU", + "XFO", + "YDD", + "YUD", + "YUM", + "YUN", + "ZAL", + "ZMK", + "ZRN", + "ZRZ", + "ZWC", + "ZWD", + "ZWN", + "ZWR", +} diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4b8dee0d956..dd156b29f22 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -1,128 +1,530 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ + from __future__ import annotations DHCP: list[dict[str, str | bool]] = [ - {"domain": "august", "hostname": "connect", "macaddress": "D86162*"}, - {"domain": "august", "hostname": "connect", "macaddress": "B8B7F1*"}, - {"domain": "august", "hostname": "connect", "macaddress": "2C9FFB*"}, - {"domain": "august", "hostname": "august*", "macaddress": "E076D0*"}, - {"domain": "awair", "macaddress": "70886B1*"}, - {"domain": "axis", "registered_devices": True}, - {"domain": "axis", "hostname": "axis-00408c*", "macaddress": "00408C*"}, - {"domain": "axis", "hostname": "axis-accc8e*", "macaddress": "ACCC8E*"}, - {"domain": "axis", "hostname": "axis-b8a44f*", "macaddress": "B8A44F*"}, - {"domain": "blink", "hostname": "blink*", "macaddress": "B85F98*"}, - {"domain": "blink", "hostname": "blink*", "macaddress": "00037F*"}, - {"domain": "blink", "hostname": "blink*", "macaddress": "20A171*"}, - {"domain": "broadlink", "registered_devices": True}, - {"domain": "broadlink", "macaddress": "34EA34*"}, - {"domain": "broadlink", "macaddress": "24DFA7*"}, - {"domain": "broadlink", "macaddress": "A043B0*"}, - {"domain": "broadlink", "macaddress": "B4430D*"}, - {"domain": "broadlink", "macaddress": "C8F742*"}, - {"domain": "elkm1", "registered_devices": True}, - {"domain": "elkm1", "macaddress": "00409D*"}, - {"domain": "emonitor", "hostname": "emonitor*", "macaddress": "0090C2*"}, - {"domain": "emonitor", "registered_devices": True}, - {"domain": "esphome", "registered_devices": True}, - {"domain": "flume", "hostname": "flume-gw-*"}, - {"domain": "flux_led", "registered_devices": True}, - {"domain": "flux_led", "macaddress": "18B905*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "249494*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "7CB94C*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "ACCF23*", "hostname": "[hba][flk]*"}, - {"domain": "flux_led", "macaddress": "B4E842*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "F0FE6B*", "hostname": "[hba][flk]*"}, - {"domain": "flux_led", "macaddress": "8CCE4E*", "hostname": "lwip*"}, - {"domain": "flux_led", "hostname": "hf-lpb100-zj*"}, - {"domain": "flux_led", "hostname": "zengge_[0-9a-f][0-9a-f]_*"}, - {"domain": "flux_led", "macaddress": "C82E47*", "hostname": "sta*"}, - {"domain": "fronius", "macaddress": "0003AC*"}, - {"domain": "fully_kiosk", "registered_devices": True}, - {"domain": "goalzero", "registered_devices": True}, - {"domain": "goalzero", "hostname": "yeti*"}, - {"domain": "gogogate2", "hostname": "ismartgate*"}, - {"domain": "guardian", "hostname": "gvc*", "macaddress": "30AEA4*"}, - {"domain": "guardian", "hostname": "gvc*", "macaddress": "B4E62D*"}, - {"domain": "guardian", "hostname": "guardian*", "macaddress": "30AEA4*"}, - {"domain": "hunterdouglas_powerview", "registered_devices": True}, + { + "domain": "airzone", + "macaddress": "E84F25*", + }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "D86162*", + }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "B8B7F1*", + }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "2C9FFB*", + }, + { + "domain": "august", + "hostname": "august*", + "macaddress": "E076D0*", + }, + { + "domain": "awair", + "macaddress": "70886B1*", + }, + { + "domain": "axis", + "registered_devices": True, + }, + { + "domain": "axis", + "hostname": "axis-00408c*", + "macaddress": "00408C*", + }, + { + "domain": "axis", + "hostname": "axis-accc8e*", + "macaddress": "ACCC8E*", + }, + { + "domain": "axis", + "hostname": "axis-b8a44f*", + "macaddress": "B8A44F*", + }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "B85F98*", + }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "00037F*", + }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "20A171*", + }, + { + "domain": "broadlink", + "registered_devices": True, + }, + { + "domain": "broadlink", + "macaddress": "34EA34*", + }, + { + "domain": "broadlink", + "macaddress": "24DFA7*", + }, + { + "domain": "broadlink", + "macaddress": "A043B0*", + }, + { + "domain": "broadlink", + "macaddress": "B4430D*", + }, + { + "domain": "broadlink", + "macaddress": "C8F742*", + }, + { + "domain": "elkm1", + "registered_devices": True, + }, + { + "domain": "elkm1", + "macaddress": "00409D*", + }, + { + "domain": "emonitor", + "hostname": "emonitor*", + "macaddress": "0090C2*", + }, + { + "domain": "emonitor", + "registered_devices": True, + }, + { + "domain": "esphome", + "registered_devices": True, + }, + { + "domain": "flume", + "hostname": "flume-gw-*", + }, + { + "domain": "flux_led", + "registered_devices": True, + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "18B905*", + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "249494*", + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "7CB94C*", + }, + { + "domain": "flux_led", + "hostname": "[hba][flk]*", + "macaddress": "ACCF23*", + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "B4E842*", + }, + { + "domain": "flux_led", + "hostname": "[hba][flk]*", + "macaddress": "F0FE6B*", + }, + { + "domain": "flux_led", + "hostname": "lwip*", + "macaddress": "8CCE4E*", + }, + { + "domain": "flux_led", + "hostname": "hf-lpb100-zj*", + }, + { + "domain": "flux_led", + "hostname": "zengge_[0-9a-f][0-9a-f]_*", + }, + { + "domain": "flux_led", + "hostname": "sta*", + "macaddress": "C82E47*", + }, + { + "domain": "fronius", + "macaddress": "0003AC*", + }, + { + "domain": "fully_kiosk", + "registered_devices": True, + }, + { + "domain": "goalzero", + "registered_devices": True, + }, + { + "domain": "goalzero", + "hostname": "yeti*", + }, + { + "domain": "gogogate2", + "hostname": "ismartgate*", + }, + { + "domain": "guardian", + "hostname": "gvc*", + "macaddress": "30AEA4*", + }, + { + "domain": "guardian", + "hostname": "gvc*", + "macaddress": "B4E62D*", + }, + { + "domain": "guardian", + "hostname": "guardian*", + "macaddress": "30AEA4*", + }, + { + "domain": "hunterdouglas_powerview", + "registered_devices": True, + }, { "domain": "hunterdouglas_powerview", "hostname": "hunter*", "macaddress": "002674*", }, - {"domain": "insteon", "macaddress": "000EF3*"}, - {"domain": "insteon", "registered_devices": True}, - {"domain": "intellifire", "hostname": "zentrios-*"}, - {"domain": "isy994", "registered_devices": True}, - {"domain": "isy994", "hostname": "isy*", "macaddress": "0021B9*"}, - {"domain": "isy994", "hostname": "polisy*", "macaddress": "000DB9*"}, - {"domain": "lametric", "registered_devices": True}, - {"domain": "lifx", "macaddress": "D073D5*"}, - {"domain": "lifx", "registered_devices": True}, - {"domain": "litterrobot", "hostname": "litter-robot4"}, - {"domain": "lyric", "hostname": "lyric-*", "macaddress": "48A2E6*"}, - {"domain": "lyric", "hostname": "lyric-*", "macaddress": "B82CA0*"}, - {"domain": "lyric", "hostname": "lyric-*", "macaddress": "00D02D*"}, - {"domain": "motion_blinds", "registered_devices": True}, - {"domain": "motion_blinds", "hostname": "motion_*"}, - {"domain": "motion_blinds", "hostname": "brel_*"}, - {"domain": "motion_blinds", "hostname": "connector_*"}, - {"domain": "myq", "macaddress": "645299*"}, - {"domain": "nest", "macaddress": "18B430*"}, - {"domain": "nest", "macaddress": "641666*"}, - {"domain": "nest", "macaddress": "D8EB46*"}, - {"domain": "nexia", "hostname": "xl857-*", "macaddress": "000231*"}, - {"domain": "nuheat", "hostname": "nuheat", "macaddress": "002338*"}, - {"domain": "nuki", "hostname": "nuki_bridge_*"}, - {"domain": "oncue", "hostname": "kohlergen*", "macaddress": "00146F*"}, - {"domain": "overkiz", "hostname": "gateway*", "macaddress": "F8811A*"}, - {"domain": "powerwall", "hostname": "1118431-*"}, - {"domain": "prusalink", "macaddress": "109C70*"}, - {"domain": "qnap_qsw", "macaddress": "245EBE*"}, - {"domain": "rachio", "hostname": "rachio-*", "macaddress": "009D6B*"}, - {"domain": "rachio", "hostname": "rachio-*", "macaddress": "F0038C*"}, - {"domain": "rachio", "hostname": "rachio-*", "macaddress": "74C63B*"}, - {"domain": "radiotherm", "hostname": "thermostat*", "macaddress": "5CDAD4*"}, - {"domain": "radiotherm", "registered_devices": True}, - {"domain": "rainforest_eagle", "macaddress": "D8D5B9*"}, - {"domain": "ring", "hostname": "ring*", "macaddress": "0CAE7D*"}, - {"domain": "roomba", "hostname": "irobot-*", "macaddress": "501479*"}, - {"domain": "roomba", "hostname": "roomba-*", "macaddress": "80A589*"}, - {"domain": "roomba", "hostname": "roomba-*", "macaddress": "DCF505*"}, - {"domain": "roomba", "hostname": "roomba-*", "macaddress": "204EF6*"}, - {"domain": "samsungtv", "registered_devices": True}, - {"domain": "samsungtv", "hostname": "tizen*"}, - {"domain": "samsungtv", "macaddress": "4844F7*"}, - {"domain": "samsungtv", "macaddress": "606BBD*"}, - {"domain": "samsungtv", "macaddress": "641CB0*"}, - {"domain": "samsungtv", "macaddress": "8CC8CD*"}, - {"domain": "samsungtv", "macaddress": "8CEA48*"}, - {"domain": "samsungtv", "macaddress": "F47B5E*"}, - {"domain": "screenlogic", "registered_devices": True}, - {"domain": "screenlogic", "hostname": "pentair*", "macaddress": "00C033*"}, - {"domain": "sense", "hostname": "sense-*", "macaddress": "009D6B*"}, - {"domain": "sense", "hostname": "sense-*", "macaddress": "DCEFCA*"}, - {"domain": "sense", "hostname": "sense-*", "macaddress": "A4D578*"}, - {"domain": "senseme", "registered_devices": True}, - {"domain": "senseme", "macaddress": "20F85E*"}, - {"domain": "sensibo", "hostname": "sensibo*"}, - {"domain": "simplisafe", "hostname": "simplisafe*", "macaddress": "30AEA4*"}, - {"domain": "sleepiq", "macaddress": "64DBA0*"}, - {"domain": "smartthings", "hostname": "st*", "macaddress": "24FD5B*"}, - {"domain": "smartthings", "hostname": "smartthings*", "macaddress": "24FD5B*"}, - {"domain": "smartthings", "hostname": "hub*", "macaddress": "24FD5B*"}, - {"domain": "smartthings", "hostname": "hub*", "macaddress": "D052A8*"}, - {"domain": "smartthings", "hostname": "hub*", "macaddress": "286D97*"}, - {"domain": "solaredge", "hostname": "target", "macaddress": "002702*"}, - {"domain": "somfy_mylink", "hostname": "somfy_*", "macaddress": "B8B7F1*"}, - {"domain": "squeezebox", "hostname": "squeezebox*", "macaddress": "000420*"}, - {"domain": "steamist", "registered_devices": True}, - {"domain": "steamist", "macaddress": "001E0C*", "hostname": "my[45]50*"}, - {"domain": "tado", "hostname": "tado*"}, + { + "domain": "insteon", + "macaddress": "000EF3*", + }, + { + "domain": "insteon", + "registered_devices": True, + }, + { + "domain": "intellifire", + "hostname": "zentrios-*", + }, + { + "domain": "isy994", + "registered_devices": True, + }, + { + "domain": "isy994", + "hostname": "isy*", + "macaddress": "0021B9*", + }, + { + "domain": "isy994", + "hostname": "polisy*", + "macaddress": "000DB9*", + }, + { + "domain": "lametric", + "registered_devices": True, + }, + { + "domain": "lifx", + "macaddress": "D073D5*", + }, + { + "domain": "lifx", + "registered_devices": True, + }, + { + "domain": "litterrobot", + "hostname": "litter-robot4", + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "48A2E6*", + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "B82CA0*", + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "00D02D*", + }, + { + "domain": "motion_blinds", + "registered_devices": True, + }, + { + "domain": "motion_blinds", + "hostname": "motion_*", + }, + { + "domain": "motion_blinds", + "hostname": "brel_*", + }, + { + "domain": "motion_blinds", + "hostname": "connector_*", + }, + { + "domain": "myq", + "macaddress": "645299*", + }, + { + "domain": "nest", + "macaddress": "18B430*", + }, + { + "domain": "nest", + "macaddress": "641666*", + }, + { + "domain": "nest", + "macaddress": "D8EB46*", + }, + { + "domain": "nexia", + "hostname": "xl857-*", + "macaddress": "000231*", + }, + { + "domain": "nuheat", + "hostname": "nuheat", + "macaddress": "002338*", + }, + { + "domain": "nuki", + "hostname": "nuki_bridge_*", + }, + { + "domain": "oncue", + "hostname": "kohlergen*", + "macaddress": "00146F*", + }, + { + "domain": "overkiz", + "hostname": "gateway*", + "macaddress": "F8811A*", + }, + { + "domain": "powerwall", + "hostname": "1118431-*", + }, + { + "domain": "powerwall", + "hostname": "1232100-*", + }, + { + "domain": "prusalink", + "macaddress": "109C70*", + }, + { + "domain": "qnap_qsw", + "macaddress": "245EBE*", + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "009D6B*", + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "F0038C*", + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "74C63B*", + }, + { + "domain": "radiotherm", + "hostname": "thermostat*", + "macaddress": "5CDAD4*", + }, + { + "domain": "radiotherm", + "registered_devices": True, + }, + { + "domain": "rainforest_eagle", + "macaddress": "D8D5B9*", + }, + { + "domain": "ring", + "hostname": "ring*", + "macaddress": "0CAE7D*", + }, + { + "domain": "roomba", + "hostname": "irobot-*", + "macaddress": "501479*", + }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "80A589*", + }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "DCF505*", + }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "204EF6*", + }, + { + "domain": "samsungtv", + "registered_devices": True, + }, + { + "domain": "samsungtv", + "hostname": "tizen*", + }, + { + "domain": "samsungtv", + "macaddress": "4844F7*", + }, + { + "domain": "samsungtv", + "macaddress": "606BBD*", + }, + { + "domain": "samsungtv", + "macaddress": "641CB0*", + }, + { + "domain": "samsungtv", + "macaddress": "8CC8CD*", + }, + { + "domain": "samsungtv", + "macaddress": "8CEA48*", + }, + { + "domain": "samsungtv", + "macaddress": "F47B5E*", + }, + { + "domain": "screenlogic", + "registered_devices": True, + }, + { + "domain": "screenlogic", + "hostname": "pentair*", + "macaddress": "00C033*", + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "009D6B*", + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "DCEFCA*", + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "A4D578*", + }, + { + "domain": "senseme", + "registered_devices": True, + }, + { + "domain": "senseme", + "macaddress": "20F85E*", + }, + { + "domain": "sensibo", + "hostname": "sensibo*", + }, + { + "domain": "simplisafe", + "hostname": "simplisafe*", + "macaddress": "30AEA4*", + }, + { + "domain": "sleepiq", + "macaddress": "64DBA0*", + }, + { + "domain": "smartthings", + "hostname": "st*", + "macaddress": "24FD5B*", + }, + { + "domain": "smartthings", + "hostname": "smartthings*", + "macaddress": "24FD5B*", + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "24FD5B*", + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "D052A8*", + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "286D97*", + }, + { + "domain": "solaredge", + "hostname": "target", + "macaddress": "002702*", + }, + { + "domain": "somfy_mylink", + "hostname": "somfy_*", + "macaddress": "B8B7F1*", + }, + { + "domain": "squeezebox", + "hostname": "squeezebox*", + "macaddress": "000420*", + }, + { + "domain": "steamist", + "registered_devices": True, + }, + { + "domain": "steamist", + "hostname": "my[45]50*", + "macaddress": "001E0C*", + }, + { + "domain": "tado", + "hostname": "tado*", + }, { "domain": "tesla_wall_connector", "hostname": "teslawallconnector_*", @@ -138,69 +540,296 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "teslawallconnector_*", "macaddress": "4CFCAA*", }, - {"domain": "tolo", "hostname": "usr-tcp232-ed2"}, - {"domain": "toon", "hostname": "eneco-*", "macaddress": "74C63B*"}, - {"domain": "tplink", "registered_devices": True}, - {"domain": "tplink", "hostname": "es*", "macaddress": "54AF97*"}, - {"domain": "tplink", "hostname": "ep*", "macaddress": "E848B8*"}, - {"domain": "tplink", "hostname": "ep*", "macaddress": "1C61B4*"}, - {"domain": "tplink", "hostname": "ep*", "macaddress": "003192*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "1C3BF3*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "50C7BF*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "68FF7B*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "98DAC4*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "B09575*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "C006C3*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "1C3BF3*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "50C7BF*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "68FF7B*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "98DAC4*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "B09575*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "60A4B7*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "005F67*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "1027F5*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "B0A7B9*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "403F8C*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "C0C9E3*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "909A4A*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "E848B8*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "003192*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "1C3BF3*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "50C7BF*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "68FF7B*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "98DAC4*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "B09575*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "C006C3*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "6C5AB0*"}, - {"domain": "tuya", "macaddress": "105A17*"}, - {"domain": "tuya", "macaddress": "10D561*"}, - {"domain": "tuya", "macaddress": "1869D8*"}, - {"domain": "tuya", "macaddress": "381F8D*"}, - {"domain": "tuya", "macaddress": "508A06*"}, - {"domain": "tuya", "macaddress": "68572D*"}, - {"domain": "tuya", "macaddress": "708976*"}, - {"domain": "tuya", "macaddress": "7CF666*"}, - {"domain": "tuya", "macaddress": "84E342*"}, - {"domain": "tuya", "macaddress": "D4A651*"}, - {"domain": "tuya", "macaddress": "D81F12*"}, - {"domain": "twinkly", "hostname": "twinkly_*"}, - {"domain": "unifiprotect", "macaddress": "B4FBE4*"}, - {"domain": "unifiprotect", "macaddress": "802AA8*"}, - {"domain": "unifiprotect", "macaddress": "F09FC2*"}, - {"domain": "unifiprotect", "macaddress": "68D79A*"}, - {"domain": "unifiprotect", "macaddress": "18E829*"}, - {"domain": "unifiprotect", "macaddress": "245A4C*"}, - {"domain": "unifiprotect", "macaddress": "784558*"}, - {"domain": "unifiprotect", "macaddress": "E063DA*"}, - {"domain": "unifiprotect", "macaddress": "265A4C*"}, - {"domain": "unifiprotect", "macaddress": "74ACB9*"}, - {"domain": "verisure", "macaddress": "0023C1*"}, - {"domain": "vicare", "macaddress": "B87424*"}, - {"domain": "wiz", "registered_devices": True}, - {"domain": "wiz", "macaddress": "A8BB50*"}, - {"domain": "wiz", "macaddress": "D8A011*"}, - {"domain": "wiz", "macaddress": "444F8E*"}, - {"domain": "wiz", "macaddress": "6C2990*"}, - {"domain": "wiz", "hostname": "wiz_*"}, - {"domain": "yeelight", "hostname": "yeelink-*"}, + { + "domain": "tolo", + "hostname": "usr-tcp232-ed2", + }, + { + "domain": "toon", + "hostname": "eneco-*", + "macaddress": "74C63B*", + }, + { + "domain": "tplink", + "registered_devices": True, + }, + { + "domain": "tplink", + "hostname": "es*", + "macaddress": "54AF97*", + }, + { + "domain": "tplink", + "hostname": "ep*", + "macaddress": "E848B8*", + }, + { + "domain": "tplink", + "hostname": "ep*", + "macaddress": "1C61B4*", + }, + { + "domain": "tplink", + "hostname": "ep*", + "macaddress": "003192*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "1C3BF3*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "50C7BF*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "68FF7B*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "98DAC4*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "B09575*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "C006C3*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "1C3BF3*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "50C7BF*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "68FF7B*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "98DAC4*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "B09575*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "60A4B7*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "005F67*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "1027F5*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "B0A7B9*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "403F8C*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "C0C9E3*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "909A4A*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "E848B8*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "003192*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "1C3BF3*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "50C7BF*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "68FF7B*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "98DAC4*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "B09575*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "C006C3*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "6C5AB0*", + }, + { + "domain": "tuya", + "macaddress": "105A17*", + }, + { + "domain": "tuya", + "macaddress": "10D561*", + }, + { + "domain": "tuya", + "macaddress": "1869D8*", + }, + { + "domain": "tuya", + "macaddress": "381F8D*", + }, + { + "domain": "tuya", + "macaddress": "508A06*", + }, + { + "domain": "tuya", + "macaddress": "68572D*", + }, + { + "domain": "tuya", + "macaddress": "708976*", + }, + { + "domain": "tuya", + "macaddress": "7CF666*", + }, + { + "domain": "tuya", + "macaddress": "84E342*", + }, + { + "domain": "tuya", + "macaddress": "D4A651*", + }, + { + "domain": "tuya", + "macaddress": "D81F12*", + }, + { + "domain": "twinkly", + "hostname": "twinkly_*", + }, + { + "domain": "unifiprotect", + "macaddress": "B4FBE4*", + }, + { + "domain": "unifiprotect", + "macaddress": "802AA8*", + }, + { + "domain": "unifiprotect", + "macaddress": "F09FC2*", + }, + { + "domain": "unifiprotect", + "macaddress": "68D79A*", + }, + { + "domain": "unifiprotect", + "macaddress": "18E829*", + }, + { + "domain": "unifiprotect", + "macaddress": "245A4C*", + }, + { + "domain": "unifiprotect", + "macaddress": "784558*", + }, + { + "domain": "unifiprotect", + "macaddress": "E063DA*", + }, + { + "domain": "unifiprotect", + "macaddress": "265A4C*", + }, + { + "domain": "unifiprotect", + "macaddress": "74ACB9*", + }, + { + "domain": "verisure", + "macaddress": "0023C1*", + }, + { + "domain": "vicare", + "macaddress": "B87424*", + }, + { + "domain": "wiz", + "registered_devices": True, + }, + { + "domain": "wiz", + "macaddress": "A8BB50*", + }, + { + "domain": "wiz", + "macaddress": "D8A011*", + }, + { + "domain": "wiz", + "macaddress": "444F8E*", + }, + { + "domain": "wiz", + "macaddress": "6C2990*", + }, + { + "domain": "wiz", + "hostname": "wiz_*", + }, + { + "domain": "yeelight", + "hostname": "yeelink-*", + }, ] diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b917153203b..02068ecafa5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -89,6 +89,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "airq": { + "name": "air-Q", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "airthings": { "name": "Airthings", "integrations": { @@ -318,6 +324,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "aranet": { + "name": "Aranet", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "arcam_fmj": { "name": "Arcam FMJ Receivers", "integration_type": "hub", @@ -605,6 +617,11 @@ "config_flow": true, "iot_class": "local_push" }, + "brandt": { + "name": "Brandt Smart Control", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "brel_home": { "name": "Brel Home", "integration_type": "virtual", @@ -944,7 +961,7 @@ }, "deluge": { "name": "Deluge", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_polling" }, @@ -1044,7 +1061,7 @@ }, "discord": { "name": "Discord", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_push" }, @@ -1614,7 +1631,7 @@ }, "flick_electric": { "name": "Flick Electric", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, @@ -1791,7 +1808,7 @@ }, "gdacs": { "name": "Global Disaster Alert and Coordination System (GDACS)", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, @@ -1847,13 +1864,13 @@ "name": "GeoNet", "integrations": { "geonetnz_quakes": { - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "GeoNet NZ Quakes" }, "geonetnz_volcano": { - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "GeoNet NZ Volcano" @@ -1915,7 +1932,7 @@ }, "goalzero": { "name": "Goal Zero Yeti", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_polling" }, @@ -1965,7 +1982,7 @@ "name": "Google Pub/Sub" }, "google_sheets": { - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "Google Sheets" @@ -2005,12 +2022,6 @@ "iot_class": "local_polling", "name": "Google Cast" }, - "hangouts": { - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_push", - "name": "Google Chat" - }, "dialogflow": { "integration_type": "hub", "config_flow": true, @@ -2142,6 +2153,11 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "hexaom": { + "name": "Hexaom Hexaconnect", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "hi_kumo": { "name": "Hitachi Hi Kumo", "integration_type": "virtual", @@ -2366,7 +2382,7 @@ }, "ign_sismologia": { "name": "IGN Sismolog\u00eda", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "cloud_polling" }, @@ -2780,7 +2796,7 @@ }, "lidarr": { "name": "Lidarr", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_polling" }, @@ -2850,12 +2866,24 @@ "config_flow": true, "iot_class": "cloud_push" }, + "livisi": { + "name": "LIVISI Smart Home", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "llamalab_automate": { "name": "LlamaLab Automate", "integration_type": "hub", "config_flow": false, "iot_class": "cloud_push" }, + "local_calendar": { + "name": "Local Calendar", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "local_file": { "name": "Local File", "integration_type": "hub", @@ -3018,6 +3046,12 @@ "config_flow": false, "iot_class": "cloud_push" }, + "matter": { + "name": "Matter (BETA)", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "mazda": { "name": "Mazda Connected Services", "integration_type": "hub", @@ -3257,7 +3291,7 @@ }, "modem_callerid": { "name": "Phone Modem", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_polling" }, @@ -3510,7 +3544,7 @@ }, "nfandroidtv": { "name": "Notifications for Android TV / Fire TV", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_push" }, @@ -3605,7 +3639,7 @@ }, "nsw_rural_fire_service_feed": { "name": "NSW Rural Fire Service Incidents", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "cloud_polling" }, @@ -3885,7 +3919,7 @@ }, "ovo_energy": { "name": "OVO Energy", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, @@ -4140,7 +4174,7 @@ "pushbullet": { "name": "Pushbullet", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "pushover": { @@ -4192,7 +4226,7 @@ }, "qld_bushfire": { "name": "Queensland Bushfire Alert", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "cloud_polling" }, @@ -4245,7 +4279,7 @@ }, "radarr": { "name": "Radarr", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_polling" }, @@ -4513,6 +4547,12 @@ } } }, + "ruuvitag_ble": { + "name": "RuuviTag BLE", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "sabnzbd": { "name": "SABnzbd", "integration_type": "hub", @@ -4535,7 +4575,7 @@ "name": "Samsung Family Hub" }, "samsungtv": { - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_push", "name": "Samsung Smart TV" @@ -4563,7 +4603,7 @@ "scrape": { "name": "Scrape", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "screenaway": { @@ -4613,6 +4653,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "sensirion_ble": { + "name": "Sensirion BLE", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "sensorblue": { "name": "SensorBlue", "integration_type": "virtual", @@ -4748,6 +4794,11 @@ "integration_type": "virtual", "supported_by": "upb" }, + "simu": { + "name": "SIMU LiveIn2", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "simulated": { "name": "Simulated", "integration_type": "hub", @@ -5074,7 +5125,7 @@ }, "steam_online": { "name": "Steam", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, @@ -5216,7 +5267,7 @@ }, "system_bridge": { "name": "System Bridge", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_push" }, @@ -5668,6 +5719,11 @@ } } }, + "ubiwizz": { + "name": "Ubiwizz", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "uk_transport": { "name": "UK Transport", "integration_type": "hub", @@ -5728,7 +5784,7 @@ }, "usgs_earthquakes_feed": { "name": "U.S. Geological Survey Earthquake Hazards (USGS)", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "cloud_polling" }, @@ -6106,16 +6162,21 @@ } }, "yamaha": { - "name": "Yamaha Network Receivers", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, - "yamaha_musiccast": { - "name": "MusicCast", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" + "name": "Yamaha", + "integrations": { + "yamaha": { + "integration_type": "hub", + "config_flow": false, + "iot_class": "local_polling", + "name": "Yamaha Network Receivers" + }, + "yamaha_musiccast": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push", + "name": "MusicCast" + } + } }, "yandex": { "name": "Yandex", @@ -6297,7 +6358,7 @@ "min_max": { "integration_type": "helper", "config_flow": true, - "iot_class": "local_push" + "iot_class": "calculated" }, "schedule": { "integration_type": "helper", diff --git a/homeassistant/generated/languages.py b/homeassistant/generated/languages.py new file mode 100644 index 00000000000..879d4a4cd41 --- /dev/null +++ b/homeassistant/generated/languages.py @@ -0,0 +1,68 @@ +"""This file is automatically generated. + +To update, run python3 -m script.languages [frontend_tag] +""" + +LANGUAGES = { + "af", + "ar", + "bg", + "bn", + "bs", + "ca", + "cs", + "cy", + "da", + "de", + "el", + "en", + "en-GB", + "eo", + "es", + "es-419", + "et", + "eu", + "fa", + "fi", + "fr", + "fy", + "gl", + "gsw", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "is", + "it", + "ja", + "ka", + "ko", + "lb", + "lt", + "lv", + "ml", + "nb", + "nl", + "nn", + "pl", + "pt", + "pt-BR", + "ro", + "ru", + "sk", + "sl", + "sr", + "sr-Latn", + "sv", + "ta", + "te", + "th", + "tr", + "uk", + "ur", + "vi", + "zh-Hans", + "zh-Hant", +} diff --git a/homeassistant/generated/mqtt.py b/homeassistant/generated/mqtt.py index 7c4203eaec2..4d4e47669c2 100644 --- a/homeassistant/generated/mqtt.py +++ b/homeassistant/generated/mqtt.py @@ -1,4 +1,4 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index c77f3f6a68b..210c0c832a2 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -1,4 +1,4 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 901f9f72da5..2d0dced8965 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -1,113 +1,119 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ USB = [ + { + "description": "*skyconnect v1.0*", + "domain": "homeassistant_sky_connect", + "pid": "EA60", + "vid": "10C4", + }, { "domain": "insteon", "vid": "10BF", }, { "domain": "modem_callerid", - "vid": "0572", "pid": "1340", + "vid": "0572", }, { "domain": "velbus", - "vid": "10CF", "pid": "0B1B", + "vid": "10CF", }, { "domain": "velbus", - "vid": "10CF", "pid": "0516", + "vid": "10CF", }, { "domain": "velbus", - "vid": "10CF", "pid": "0517", + "vid": "10CF", }, { "domain": "velbus", - "vid": "10CF", "pid": "0518", + "vid": "10CF", }, { - "domain": "zha", - "vid": "10C4", - "pid": "EA60", "description": "*2652*", + "domain": "zha", + "pid": "EA60", + "vid": "10C4", }, { + "description": "*sonoff*plus*", "domain": "zha", - "vid": "1A86", "pid": "55D4", - "description": "*sonoff*plus*", - }, - { - "domain": "zha", - "vid": "10C4", - "pid": "EA60", - "description": "*sonoff*plus*", - }, - { - "domain": "zha", - "vid": "10C4", - "pid": "EA60", - "description": "*tubeszb*", - }, - { - "domain": "zha", "vid": "1A86", - "pid": "7523", - "description": "*tubeszb*", }, { + "description": "*sonoff*plus*", + "domain": "zha", + "pid": "EA60", + "vid": "10C4", + }, + { + "description": "*tubeszb*", + "domain": "zha", + "pid": "EA60", + "vid": "10C4", + }, + { + "description": "*tubeszb*", "domain": "zha", - "vid": "1A86", "pid": "7523", + "vid": "1A86", + }, + { "description": "*zigstar*", + "domain": "zha", + "pid": "7523", + "vid": "1A86", }, { - "domain": "zha", - "vid": "1CF1", - "pid": "0030", "description": "*conbee*", + "domain": "zha", + "pid": "0030", + "vid": "1CF1", }, { - "domain": "zha", - "vid": "10C4", - "pid": "8A2A", "description": "*zigbee*", - }, - { "domain": "zha", - "vid": "0403", - "pid": "6015", - "description": "*zigate*", - }, - { - "domain": "zha", - "vid": "10C4", - "pid": "EA60", - "description": "*zigate*", - }, - { - "domain": "zha", - "vid": "10C4", - "pid": "8B34", - "description": "*bv 2010/10*", - }, - { - "domain": "zwave_js", - "vid": "0658", - "pid": "0200", - }, - { - "domain": "zwave_js", - "vid": "10C4", "pid": "8A2A", + "vid": "10C4", + }, + { + "description": "*zigate*", + "domain": "zha", + "pid": "6015", + "vid": "0403", + }, + { + "description": "*zigate*", + "domain": "zha", + "pid": "EA60", + "vid": "10C4", + }, + { + "description": "*bv 2010/10*", + "domain": "zha", + "pid": "8B34", + "vid": "10C4", + }, + { + "domain": "zwave_js", + "pid": "0200", + "vid": "0658", + }, + { "description": "*z-wave*", + "domain": "zwave_js", + "pid": "8A2A", + "vid": "10C4", }, ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index fc0c3ea5fa7..0d505bd2409 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -1,8 +1,70 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ +HOMEKIT = { + "3810X": "roku", + "3820X": "roku", + "4660X": "roku", + "7820X": "roku", + "819LMB": "myq", + "AC02": "tado", + "Abode": "abode", + "BSB002": "hue", + "C105X": "roku", + "C135X": "roku", + "EB-*": "ecobee", + "Escea": "escea", + "HHKBridge*": "hive", + "Healty Home Coach": "netatmo", + "Iota": "abode", + "LIFX A19": "lifx", + "LIFX BR30": "lifx", + "LIFX Beam": "lifx", + "LIFX Candle": "lifx", + "LIFX Clean": "lifx", + "LIFX Color": "lifx", + "LIFX DLCOL": "lifx", + "LIFX DLWW": "lifx", + "LIFX Dlight": "lifx", + "LIFX Downlight": "lifx", + "LIFX Filament": "lifx", + "LIFX GU10": "lifx", + "LIFX Lightstrip": "lifx", + "LIFX Mini": "lifx", + "LIFX Nightvision": "lifx", + "LIFX Pls": "lifx", + "LIFX Plus": "lifx", + "LIFX Tile": "lifx", + "LIFX White": "lifx", + "LIFX Z": "lifx", + "MYQ": "myq", + "NL29": "nanoleaf", + "NL42": "nanoleaf", + "NL47": "nanoleaf", + "NL48": "nanoleaf", + "NL52": "nanoleaf", + "NL59": "nanoleaf", + "Netatmo Relay": "netatmo", + "PowerView": "hunterdouglas_powerview", + "Presence": "netatmo", + "Rachio": "rachio", + "SPK5": "rainmachine", + "Sensibo": "sensibo", + "Smart Bridge": "lutron_caseta", + "Socket": "wemo", + "TRADFRI": "tradfri", + "Touch HD": "rainmachine", + "Welcome": "netatmo", + "Wemo": "wemo", + "YL*": "yeelight", + "ecobee*": "ecobee", + "iSmartGate": "gogogate2", + "iZone": "izone", + "tado": "tado", +} + ZEROCONF = { "_Volumio._tcp.local.": [ { @@ -436,65 +498,3 @@ ZEROCONF = { }, ], } - -HOMEKIT = { - "3810X": "roku", - "3820X": "roku", - "4660X": "roku", - "7820X": "roku", - "819LMB": "myq", - "AC02": "tado", - "Abode": "abode", - "BSB002": "hue", - "C105X": "roku", - "C135X": "roku", - "EB-*": "ecobee", - "Escea": "escea", - "HHKBridge*": "hive", - "Healty Home Coach": "netatmo", - "Iota": "abode", - "LIFX A19": "lifx", - "LIFX BR30": "lifx", - "LIFX Beam": "lifx", - "LIFX Candle": "lifx", - "LIFX Clean": "lifx", - "LIFX Color": "lifx", - "LIFX DLCOL": "lifx", - "LIFX DLWW": "lifx", - "LIFX Dlight": "lifx", - "LIFX Downlight": "lifx", - "LIFX Filament": "lifx", - "LIFX GU10": "lifx", - "LIFX Lightstrip": "lifx", - "LIFX Mini": "lifx", - "LIFX Nightvision": "lifx", - "LIFX Pls": "lifx", - "LIFX Plus": "lifx", - "LIFX Tile": "lifx", - "LIFX White": "lifx", - "LIFX Z": "lifx", - "MYQ": "myq", - "NL29": "nanoleaf", - "NL42": "nanoleaf", - "NL47": "nanoleaf", - "NL48": "nanoleaf", - "NL52": "nanoleaf", - "NL59": "nanoleaf", - "Netatmo Relay": "netatmo", - "PowerView": "hunterdouglas_powerview", - "Presence": "netatmo", - "Rachio": "rachio", - "SPK5": "rainmachine", - "Sensibo": "sensibo", - "Smart Bridge": "lutron_caseta", - "Socket": "wemo", - "TRADFRI": "tradfri", - "Touch HD": "rainmachine", - "Welcome": "netatmo", - "Wemo": "wemo", - "YL*": "yeelight", - "ecobee*": "ecobee", - "iSmartGate": "gogogate2", - "iZone": "izone", - "tado": "tado", -} diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 3bda89c9a73..bc17330f656 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -122,7 +122,7 @@ async def async_check_ha_config_file( # noqa: C901 core_config.pop(CONF_PACKAGES, None) # Filter out repeating config sections - components = {key.split(" ")[0] for key in config.keys()} + components = {key.partition(" ")[0] for key in config.keys()} # Process and validate config for domain in components: diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 35191d77042..fc71a586aee 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -88,6 +88,9 @@ from homeassistant.const import ( ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError +from homeassistant.generated import currencies +from homeassistant.generated.countries import COUNTRIES +from homeassistant.generated.languages import LANGUAGES from homeassistant.util import raise_if_invalid_path, slugify as util_slugify import homeassistant.util.dt as dt_util @@ -593,7 +596,7 @@ def template(value: Any | None) -> template_helper.Template: if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") - template_value = template_helper.Template(str(value)) # type: ignore[no-untyped-call] + template_value = template_helper.Template(str(value)) try: template_value.ensure_valid() @@ -611,7 +614,7 @@ def dynamic_template(value: Any | None) -> template_helper.Template: if not template_helper.is_template_string(str(value)): raise vol.Invalid("template value does not contain a dynamic template") - template_value = template_helper.Template(str(value)) # type: ignore[no-untyped-call] + template_value = template_helper.Template(str(value)) try: template_value.ensure_valid() return template_value @@ -913,9 +916,9 @@ def key_value_schemas( with contextlib.suppress(vol.Invalid): return cast(dict[Hashable, Any], default_schema(value)) - alternatives = ", ".join(str(key) for key in value_schemas) + alternatives = ", ".join(str(alternative) for alternative in value_schemas) if default_description: - alternatives += ", " + default_description + alternatives = f"{alternatives}, {default_description}" raise vol.Invalid( f"Unexpected value for {key}: '{key_value}'. Expected {alternatives}" ) @@ -1654,167 +1657,14 @@ ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = { } -# Validate currencies adopted by countries currency = vol.In( - { - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTN", - "BWP", - "BYN", - "BYR", - "BZD", - "CAD", - "CDF", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CUP", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ERN", - "ETB", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "INR", - "IQD", - "IRR", - "ISK", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KPW", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTL", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SDG", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "SSP", - "STD", - "SYP", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XCD", - "XOF", - "XPF", - "YER", - "ZAR", - "ZMK", - "ZMW", - "ZWL", - }, - msg="invalid ISO 4217 formatted currency", + currencies.ACTIVE_CURRENCIES, msg="invalid ISO 4217 formatted currency" ) + +historic_currency = vol.In( + currencies.HISTORIC_CURRENCIES, msg="invalid ISO 4217 formatted historic currency" +) + +country = vol.In(COUNTRIES, msg="invalid ISO 3166 formatted country") + +language = vol.In(LANGUAGES, msg="invalid RFC 5646 formatted language") diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 57cfe362231..255b0c2d834 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -144,7 +144,7 @@ def get_supported_features(hass: HomeAssistant, entity_id: str) -> int: def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: - """Get unit of measurement class of an entity. + """Get unit of measurement of an entity. First try the statemachine, then entity registry. """ @@ -705,9 +705,9 @@ class Entity(ABC): try: task: asyncio.Future[None] if hasattr(self, "async_update"): - task = self.hass.async_create_task(self.async_update()) # type: ignore[attr-defined] + task = self.hass.async_create_task(self.async_update()) elif hasattr(self, "update"): - task = self.hass.async_add_executor_job(self.update) # type: ignore[attr-defined] + task = self.hass.async_add_executor_job(self.update) else: return diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index eea0a9943a3..1932c0397a1 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -36,7 +36,7 @@ _EntityT = TypeVar("_EntityT", bound=entity.Entity) @bind_hass async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: """Trigger an update for an entity.""" - domain = entity_id.split(".", 1)[0] + domain = entity_id.partition(".")[0] entity_comp: EntityComponent[entity.Entity] | None entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) @@ -90,7 +90,12 @@ class EntityComponent(Generic[_EntityT]): @property def entities(self) -> Iterable[_EntityT]: - """Return an iterable that returns all entities.""" + """ + Return an iterable that returns all entities. + + As the underlying dicts may change when async context is lost, callers that + iterate over this asynchronously should make a copy using list() before iterating. + """ return chain.from_iterable( platform.entities.values() # type: ignore[misc] for platform in self._platforms.values() diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 613b6fb3227..b553b855be1 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta import functools as ft import logging +from random import randint import time from typing import Any, Union, cast @@ -60,6 +61,9 @@ _ENTITIES_LISTENER = "entities" _LOGGER = logging.getLogger(__name__) +RANDOM_MICROSECOND_MIN = 50000 +RANDOM_MICROSECOND_MAX = 500000 + _P = ParamSpec("_P") @@ -1506,13 +1510,17 @@ def async_track_utc_time_change( matching_seconds = dt_util.parse_time_expression(second, 0, 59) matching_minutes = dt_util.parse_time_expression(minute, 0, 59) matching_hours = dt_util.parse_time_expression(hour, 0, 23) + # Avoid aligning all time trackers to the same second + # since it can create a thundering herd problem + # https://github.com/home-assistant/core/issues/82231 + microsecond = randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) def calculate_next(now: datetime) -> datetime: """Calculate and set the next time the trigger should fire.""" localized_now = dt_util.as_local(now) if local else now return dt_util.find_next_time_expression_time( localized_now, matching_seconds, matching_minutes, matching_hours - ) + ).replace(microsecond=microsecond) time_listener: CALLBACK_TYPE | None = None diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 413161eb150..8980abbb466 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Callable, Mapping +from collections.abc import Callable, Coroutine, Mapping import copy from dataclasses import dataclass import types @@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.data_entry_flow import FlowResult, UnknownHandler from . import entity_registry as er, selector +from .typing import UNDEFINED, UndefinedType class SchemaFlowError(Exception): @@ -22,46 +23,64 @@ class SchemaFlowError(Exception): @dataclass -class SchemaFlowFormStep: +class SchemaFlowStep: """Define a config or options flow step.""" - # Optional schema for requesting and validating user input. If schema validation - # fails, the step will be retried. If the schema is None, no user input is requested. - schema: vol.Schema | Callable[ - [SchemaConfigFlowHandler | SchemaOptionsFlowHandler, dict[str, Any]], - vol.Schema | None, - ] | None - - # Optional function to validate user input. - # The validate_user_input function is called if the schema validates successfully. - # The validate_user_input function is passed the user input from the current step. - # The validate_user_input should raise SchemaFlowError is user input is invalid. - validate_user_input: Callable[[dict[str, Any]], dict[str, Any]] = lambda x: x - - # Optional function to identify next step. - # The next_step function is called if the schema validates successfully or if no - # schema is defined. The next_step function is passed the union of config entry - # options and user input from previous steps. - # If next_step returns None, the flow is ended with FlowResultType.CREATE_ENTRY. - next_step: Callable[[dict[str, Any]], str | None] = lambda _: None - - # Optional function to allow amending a form schema. - # The update_form_schema function is called before async_show_form is called. The - # update_form_schema function is passed the handler, which is either an instance of - # SchemaConfigFlowHandler or SchemaOptionsFlowHandler, the schema, and the union of - # config entry options and user input from previous steps. - update_form_schema: Callable[ - [ - SchemaConfigFlowHandler | SchemaOptionsFlowHandler, - vol.Schema, - dict[str, Any], - ], - vol.Schema, - ] = lambda _handler, schema, _options: schema - @dataclass -class SchemaFlowMenuStep: +class SchemaFlowFormStep(SchemaFlowStep): + """Define a config or options flow form step.""" + + schema: vol.Schema | Callable[ + [SchemaCommonFlowHandler], Coroutine[Any, Any, vol.Schema | None] + ] | None = None + """Optional voluptuous schema, or function which returns a schema or None, for + requesting and validating user input. + + - If a function is specified, the function will be passed the current + `SchemaCommonFlowHandler`. + - If schema validation fails, the step will be retried. If the schema is None, no + user input is requested. + """ + + validate_user_input: Callable[ + [SchemaCommonFlowHandler, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]] + ] | None = None + """Optional function to validate user input. + + - The `validate_user_input` function is called if the schema validates successfully. + - The first argument is a reference to the current `SchemaCommonFlowHandler`. + - The second argument is the user input from the current step. + - The `validate_user_input` should raise `SchemaFlowError` if user input is invalid. + """ + + next_step: Callable[ + [dict[str, Any]], Coroutine[Any, Any, str | None] + ] | str | None = None + """Optional property to identify next step. + + - If `next_step` is a function, it is called if the schema validates successfully or + if no schema is defined. The `next_step` function is passed the union of config entry + options and user input from previous steps. If the function returns None, the flow is + ended with `FlowResultType.CREATE_ENTRY`. + - If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`. + """ + + suggested_values: Callable[ + [SchemaCommonFlowHandler], Coroutine[Any, Any, dict[str, Any]] + ] | None | UndefinedType = UNDEFINED + """Optional property to populate suggested values. + + - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested value + from an option with the same key. + + Note: if a step is retried due to a validation failure, then the user input will have + priority over the suggested values. + """ + + +@dataclass +class SchemaFlowMenuStep(SchemaFlowStep): """Define a config or options flow menu step.""" # Menu options @@ -74,13 +93,33 @@ class SchemaCommonFlowHandler: def __init__( self, handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, - flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep], - config_entry: config_entries.ConfigEntry | None, + flow: Mapping[str, SchemaFlowStep], + options: dict[str, Any] | None, ) -> None: """Initialize a common handler.""" self._flow = flow self._handler = handler - self._options = dict(config_entry.options) if config_entry is not None else {} + self._options = options if options is not None else {} + self._flow_state: dict[str, Any] = {} + + @property + def parent_handler(self) -> SchemaConfigFlowHandler | SchemaOptionsFlowHandler: + """Return parent handler.""" + return self._handler + + @property + def options(self) -> dict[str, Any]: + """Return the options linked to the current flow handler.""" + return self._options + + @property + def flow_state(self) -> dict[str, Any]: + """Return the flow state, used to store temporary data. + + It can be used for example to store the key or the index of a sub-item + that will be edited in the next step. + """ + return self._flow_state async def async_step( self, step_id: str, user_input: dict[str, Any] | None = None @@ -90,14 +129,12 @@ class SchemaCommonFlowHandler: return await self._async_form_step(step_id, user_input) return await self._async_menu_step(step_id, user_input) - def _get_schema( - self, form_step: SchemaFlowFormStep, options: dict[str, Any] - ) -> vol.Schema | None: + async def _get_schema(self, form_step: SchemaFlowFormStep) -> vol.Schema | None: if form_step.schema is None: return None if isinstance(form_step.schema, vol.Schema): return form_step.schema - return form_step.schema(self._handler, options) + return await form_step.schema(self) async def _async_form_step( self, step_id: str, user_input: dict[str, Any] | None = None @@ -107,7 +144,7 @@ class SchemaCommonFlowHandler: if ( user_input is not None - and (data_schema := self._get_schema(form_step, self._options)) + and (data_schema := await self._get_schema(form_step)) and data_schema.schema and not self._handler.show_advanced_options ): @@ -122,91 +159,102 @@ class SchemaCommonFlowHandler: ): user_input[str(key.schema)] = key.default() - if user_input is not None and form_step.schema is not None: + if user_input is not None and form_step.validate_user_input is not None: # Do extra validation of user input try: - user_input = form_step.validate_user_input(user_input) + user_input = await form_step.validate_user_input(self, user_input) except SchemaFlowError as exc: - return self._show_next_step(step_id, exc, user_input) + return await self._show_next_step(step_id, exc, user_input) if user_input is not None: # User input was validated successfully, update options self._options.update(user_input) - next_step_id: str = step_id if user_input is not None or form_step.schema is None: - # Get next step - next_step_id_or_end_flow = form_step.next_step(self._options) - if next_step_id_or_end_flow is None: - # Flow done, create entry or update config entry options - return self._handler.async_create_entry(data=self._options) + return await self._show_next_step_or_create_entry(form_step) - next_step_id = next_step_id_or_end_flow + return await self._show_next_step(step_id) - return self._show_next_step(next_step_id) + async def _show_next_step_or_create_entry( + self, form_step: SchemaFlowFormStep + ) -> FlowResult: + next_step_id_or_end_flow: str | None - def _show_next_step( + if callable(form_step.next_step): + next_step_id_or_end_flow = await form_step.next_step(self._options) + else: + next_step_id_or_end_flow = form_step.next_step + + if next_step_id_or_end_flow is None: + # Flow done, create entry or update config entry options + return self._handler.async_create_entry(data=self._options) + return await self._show_next_step(next_step_id_or_end_flow) + + async def _show_next_step( self, next_step_id: str, error: SchemaFlowError | None = None, user_input: dict[str, Any] | None = None, ) -> FlowResult: """Show form for next step.""" - form_step: SchemaFlowFormStep = cast( - SchemaFlowFormStep, self._flow[next_step_id] - ) + if isinstance(self._flow[next_step_id], SchemaFlowMenuStep): + menu_step = cast(SchemaFlowMenuStep, self._flow[next_step_id]) + return self._handler.async_show_menu( + step_id=next_step_id, + menu_options=menu_step.options, + ) + + form_step = cast(SchemaFlowFormStep, self._flow[next_step_id]) + + if (data_schema := await self._get_schema(form_step)) is None: + return await self._show_next_step_or_create_entry(form_step) + + suggested_values: dict[str, Any] = {} + if form_step.suggested_values is UNDEFINED: + suggested_values = self._options + elif form_step.suggested_values: + suggested_values = await form_step.suggested_values(self) - options = dict(self._options) if user_input: - options.update(user_input) + # We don't want to mutate the existing options + suggested_values = copy.deepcopy(suggested_values) + suggested_values.update(user_input) - if ( - data_schema := self._get_schema(form_step, self._options) - ) and data_schema.schema: + if data_schema.schema: # Make a copy of the schema with suggested values set to saved options - schema = {} - for key, val in data_schema.schema.items(): - - if isinstance(key, vol.Marker): - # Exclude advanced field - if ( - key.description - and key.description.get("advanced") - and not self._handler.show_advanced_options - ): - continue - - new_key = key - if key in options and isinstance(key, vol.Marker): - # Copy the marker to not modify the flow schema - new_key = copy.copy(key) - new_key.description = {"suggested_value": options[key]} - schema[new_key] = val - data_schema = vol.Schema(schema) + data_schema = self._handler.add_suggested_values_to_schema( + data_schema, suggested_values + ) errors = {"base": str(error)} if error else None # Show form for next step + last_step = None + if not callable(form_step.next_step): + last_step = form_step.next_step is None return self._handler.async_show_form( - step_id=next_step_id, data_schema=data_schema, errors=errors + step_id=next_step_id, + data_schema=data_schema, + errors=errors, + last_step=last_step, ) async def _async_menu_step( self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a menu step.""" - form_step: SchemaFlowMenuStep = cast(SchemaFlowMenuStep, self._flow[step_id]) + menu_step: SchemaFlowMenuStep = cast(SchemaFlowMenuStep, self._flow[step_id]) return self._handler.async_show_menu( step_id=step_id, - menu_options=form_step.options, + menu_options=menu_step.options, ) class SchemaConfigFlowHandler(config_entries.ConfigFlow): """Handle a schema based config flow.""" - config_flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] - options_flow: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] | None = None + config_flow: Mapping[str, SchemaFlowStep] + options_flow: Mapping[str, SchemaFlowStep] | None = None VERSION = 1 @@ -300,18 +348,27 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow): ) -class SchemaOptionsFlowHandler(config_entries.OptionsFlow): +class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): """Handle a schema based options flow.""" def __init__( self, config_entry: config_entries.ConfigEntry, - options_flow: dict[str, vol.Schema], - async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None], + options_flow: Mapping[str, SchemaFlowStep], + async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None] + | None = None, ) -> None: - """Initialize options flow.""" - self._common_handler = SchemaCommonFlowHandler(self, options_flow, config_entry) - self.config_entry = config_entry + """Initialize options flow. + + If needed, `async_options_flow_finished` can be set to take necessary actions + after the options flow is finished. The second parameter contains config entry + options, which is the union of stored options and user input from the options + flow steps. + """ + super().__init__(config_entry) + self._common_handler = SchemaCommonFlowHandler( + self, options_flow, self._options + ) self._async_options_flow_finished = async_options_flow_finished for step in options_flow: @@ -342,7 +399,8 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlow): **kwargs: Any, ) -> FlowResult: """Finish config flow and create a config entry.""" - self._async_options_flow_finished(self.hass, data) + if self._async_options_flow_finished: + self._async_options_flow_finished(self.hass, data) return super().async_create_entry(title="", data=data, **kwargs) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index bd6dff03858..c7a4be175fb 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1,8 +1,8 @@ """Selectors for Home Assistant.""" from __future__ import annotations -from collections.abc import Callable, Sequence -from typing import Any, Literal, TypedDict, cast +from collections.abc import Callable, Mapping, Sequence +from typing import Any, Generic, Literal, TypedDict, TypeVar, cast from uuid import UUID import voluptuous as vol @@ -17,6 +17,8 @@ from . import config_validation as cv SELECTORS: decorator.Registry[str, type[Selector]] = decorator.Registry() +_T = TypeVar("_T", bound=Mapping[str, Any]) + def _get_selector_class(config: Any) -> type[Selector]: """Get selector class type.""" @@ -56,14 +58,14 @@ def validate_selector(config: Any) -> dict: } -class Selector: +class Selector(Generic[_T]): """Base class for selectors.""" CONFIG_SCHEMA: Callable - config: Any + config: _T selector_type: str - def __init__(self, config: Any = None) -> None: + def __init__(self, config: Mapping[str, Any] | None = None) -> None: """Instantiate a selector.""" # Selectors can be empty if config is None: @@ -71,7 +73,7 @@ class Selector: self.config = self.CONFIG_SCHEMA(config) - def serialize(self) -> Any: + def serialize(self) -> dict[str, dict[str, _T]]: """Serialize Selector for voluptuous_serialize.""" return {"selector": {self.selector_type: self.config}} @@ -92,7 +94,7 @@ class SingleEntitySelectorConfig(TypedDict, total=False): """Class to represent a single entity selector config.""" integration: str - domain: str + domain: str | list[str] device_class: str @@ -124,7 +126,7 @@ class ActionSelectorConfig(TypedDict): @SELECTORS.register("action") -class ActionSelector(Selector): +class ActionSelector(Selector[ActionSelectorConfig]): """Selector of an action sequence (script syntax).""" selector_type = "action" @@ -148,7 +150,7 @@ class AddonSelectorConfig(TypedDict, total=False): @SELECTORS.register("addon") -class AddonSelector(Selector): +class AddonSelector(Selector[AddonSelectorConfig]): """Selector of a add-on.""" selector_type = "addon" @@ -179,7 +181,7 @@ class AreaSelectorConfig(TypedDict, total=False): @SELECTORS.register("area") -class AreaSelector(Selector): +class AreaSelector(Selector[AreaSelectorConfig]): """Selector of a single or list of areas.""" selector_type = "area" @@ -214,7 +216,7 @@ class AttributeSelectorConfig(TypedDict, total=False): @SELECTORS.register("attribute") -class AttributeSelector(Selector): +class AttributeSelector(Selector[AttributeSelectorConfig]): """Selector for an entity attribute.""" selector_type = "attribute" @@ -243,7 +245,7 @@ class BooleanSelectorConfig(TypedDict): @SELECTORS.register("boolean") -class BooleanSelector(Selector): +class BooleanSelector(Selector[BooleanSelectorConfig]): """Selector of a boolean value.""" selector_type = "boolean" @@ -265,7 +267,7 @@ class ColorRGBSelectorConfig(TypedDict): @SELECTORS.register("color_rgb") -class ColorRGBSelector(Selector): +class ColorRGBSelector(Selector[ColorRGBSelectorConfig]): """Selector of an RGB color value.""" selector_type = "color_rgb" @@ -290,7 +292,7 @@ class ColorTempSelectorConfig(TypedDict, total=False): @SELECTORS.register("color_temp") -class ColorTempSelector(Selector): +class ColorTempSelector(Selector[ColorTempSelectorConfig]): """Selector of an color temperature.""" selector_type = "color_temp" @@ -325,7 +327,7 @@ class ConfigEntrySelectorConfig(TypedDict, total=False): @SELECTORS.register("config_entry") -class ConfigEntrySelector(Selector): +class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]): """Selector of a config entry.""" selector_type = "config_entry" @@ -351,7 +353,7 @@ class DateSelectorConfig(TypedDict): @SELECTORS.register("date") -class DateSelector(Selector): +class DateSelector(Selector[DateSelectorConfig]): """Selector of a date.""" selector_type = "date" @@ -373,7 +375,7 @@ class DateTimeSelectorConfig(TypedDict): @SELECTORS.register("datetime") -class DateTimeSelector(Selector): +class DateTimeSelector(Selector[DateTimeSelectorConfig]): """Selector of a datetime.""" selector_type = "datetime" @@ -401,7 +403,7 @@ class DeviceSelectorConfig(TypedDict, total=False): @SELECTORS.register("device") -class DeviceSelector(Selector): +class DeviceSelector(Selector[DeviceSelectorConfig]): """Selector of a single or list of devices.""" selector_type = "device" @@ -431,7 +433,7 @@ class DurationSelectorConfig(TypedDict, total=False): @SELECTORS.register("duration") -class DurationSelector(Selector): +class DurationSelector(Selector[DurationSelectorConfig]): """Selector for a duration.""" selector_type = "duration" @@ -463,7 +465,7 @@ class EntitySelectorConfig(SingleEntitySelectorConfig, total=False): @SELECTORS.register("entity") -class EntitySelector(Selector): +class EntitySelector(Selector[EntitySelectorConfig]): """Selector of a single or list of entities.""" selector_type = "entity" @@ -517,7 +519,7 @@ class IconSelectorConfig(TypedDict, total=False): @SELECTORS.register("icon") -class IconSelector(Selector): +class IconSelector(Selector[IconSelectorConfig]): """Selector for an icon.""" selector_type = "icon" @@ -545,7 +547,7 @@ class LocationSelectorConfig(TypedDict, total=False): @SELECTORS.register("location") -class LocationSelector(Selector): +class LocationSelector(Selector[LocationSelectorConfig]): """Selector for a location.""" selector_type = "location" @@ -576,7 +578,7 @@ class MediaSelectorConfig(TypedDict): @SELECTORS.register("media") -class MediaSelector(Selector): +class MediaSelector(Selector[MediaSelectorConfig]): """Selector for media.""" selector_type = "media" @@ -636,7 +638,7 @@ def validate_slider(data: Any) -> Any: @SELECTORS.register("number") -class NumberSelector(Selector): +class NumberSelector(Selector[NumberSelectorConfig]): """Selector of a numeric value.""" selector_type = "number" @@ -682,7 +684,7 @@ class ObjectSelectorConfig(TypedDict): @SELECTORS.register("object") -class ObjectSelector(Selector): +class ObjectSelector(Selector[ObjectSelectorConfig]): """Selector for an arbitrary object.""" selector_type = "object" @@ -733,7 +735,7 @@ class SelectSelectorConfig(TypedDict, total=False): @SELECTORS.register("select") -class SelectSelector(Selector): +class SelectSelector(Selector[SelectSelectorConfig]): """Selector for an single-choice input select.""" selector_type = "select" @@ -755,12 +757,15 @@ class SelectSelector(Selector): def __call__(self, data: Any) -> Any: """Validate the passed selection.""" - options = [] - if self.config["options"]: - if isinstance(self.config["options"][0], str): - options = self.config["options"] + options: Sequence[str] = [] + if config_options := self.config["options"]: + if isinstance(config_options[0], str): + options = cast(Sequence[str], config_options) else: - options = [option["value"] for option in self.config["options"]] + options = [ + option["value"] + for option in cast(Sequence[SelectOptionDict], config_options) + ] parent_schema = vol.In(options) if self.config["custom_value"]: @@ -787,7 +792,7 @@ class StateSelectorConfig(TypedDict, total=False): @SELECTORS.register("state") -class StateSelector(Selector): +class StateSelector(Selector[StateSelectorConfig]): """Selector for an entity state.""" selector_type = "state" @@ -814,7 +819,7 @@ class StateSelector(Selector): @SELECTORS.register("target") -class TargetSelector(Selector): +class TargetSelector(Selector[TargetSelectorConfig]): """Selector of a target value (area ID, device ID, entity ID etc). Value should follow cv.TARGET_SERVICE_FIELDS format. @@ -846,7 +851,7 @@ class TemplateSelectorConfig(TypedDict): @SELECTORS.register("template") -class TemplateSelector(Selector): +class TemplateSelector(Selector[TemplateSelectorConfig]): """Selector for an template.""" selector_type = "template" @@ -869,6 +874,7 @@ class TextSelectorConfig(TypedDict, total=False): multiline: bool suffix: str type: TextSelectorType + autocomplete: str class TextSelectorType(StrEnum): @@ -890,7 +896,7 @@ class TextSelectorType(StrEnum): @SELECTORS.register("text") -class TextSelector(Selector): +class TextSelector(Selector[TextSelectorConfig]): """Selector for a multi-line text string.""" selector_type = "text" @@ -904,6 +910,7 @@ class TextSelector(Selector): vol.Optional("type"): vol.All( vol.Coerce(TextSelectorType), lambda val: val.value ), + vol.Optional("autocomplete"): str, } ) @@ -922,7 +929,7 @@ class ThemeSelectorConfig(TypedDict): @SELECTORS.register("theme") -class ThemeSelector(Selector): +class ThemeSelector(Selector[ThemeSelectorConfig]): """Selector for an theme.""" selector_type = "theme" @@ -944,7 +951,7 @@ class TimeSelectorConfig(TypedDict): @SELECTORS.register("time") -class TimeSelector(Selector): +class TimeSelector(Selector[TimeSelectorConfig]): """Selector of a time value.""" selector_type = "time" @@ -968,7 +975,7 @@ class FileSelectorConfig(TypedDict): @SELECTORS.register("file") -class FileSelector(Selector): +class FileSelector(Selector[FileSelectorConfig]): """Selector of a file.""" selector_type = "file" diff --git a/homeassistant/helpers/sensor.py b/homeassistant/helpers/sensor.py new file mode 100644 index 00000000000..f206ac55bdd --- /dev/null +++ b/homeassistant/helpers/sensor.py @@ -0,0 +1,28 @@ +"""Common functions related to sensor device management.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant import const + +from .entity import DeviceInfo + +if TYPE_CHECKING: + # `sensor_state_data` is a second-party library (i.e. maintained by Home Assistant + # core members) which is not strictly required by Home Assistant. + # Therefore, we import it as a type hint only. + from sensor_state_data import SensorDeviceInfo + + +def sensor_device_info_to_hass_device_info( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor_state_data sensor device info to a Home Assistant device info.""" + device_info = DeviceInfo() + if sensor_device_info.name is not None: + device_info[const.ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + device_info[const.ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + device_info[const.ATTR_MODEL] = sensor_device_info.model + return device_info diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 138fa739794..74f8d088ffc 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -202,7 +202,7 @@ def async_prepare_call_from_config( f"Template rendered invalid service: {domain_service}" ) from ex - domain, service = domain_service.split(".", 1) + domain, _, service = domain_service.partition(".") target = {} if CONF_TARGET in config: @@ -437,7 +437,7 @@ def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_T def _load_services_files( hass: HomeAssistant, integrations: Iterable[Integration] ) -> list[JSON_TYPE]: - """Load service files for multiple intergrations.""" + """Load service files for multiple integrations.""" return [_load_services_file(hass, integration) for integration in integrations] diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 6819a1eb48b..44a0da7866b 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -177,6 +177,7 @@ class Store(Generic[_T]): if data["version"] != self.version: raise stored = data["data"] + await self.async_save(stored) return stored diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index dfab80e5223..73e1400cedf 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -19,7 +19,8 @@ import re import statistics from struct import error as StructError, pack, unpack_from import sys -from typing import Any, NoReturn, TypeVar, cast, overload +from types import CodeType +from typing import Any, Literal, NoReturn, TypeVar, cast, overload from urllib.parse import urlencode as urllib_urlencode import weakref @@ -28,6 +29,7 @@ import jinja2 from jinja2 import pass_context, pass_environment, pass_eval_context from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.const import ( @@ -40,6 +42,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import ( + Context, HomeAssistant, State, callback, @@ -94,6 +97,8 @@ _COLLECTABLE_STATE_ATTRIBUTES = { } _T = TypeVar("_T") +_R = TypeVar("_R") +_P = ParamSpec("_P") ALL_STATES_RATE_LIMIT = timedelta(minutes=1) DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1) @@ -168,10 +173,10 @@ class ResultWrapper: render_result: str | None -def gen_result_wrapper(kls): +def gen_result_wrapper(kls: type[dict | list | set]) -> type: """Generate a result wrapper.""" - class Wrapper(kls, ResultWrapper): + class Wrapper(kls, ResultWrapper): # type: ignore[valid-type,misc] """Wrapper of a kls that can store render_result.""" def __init__(self, *args: Any, render_result: str | None = None) -> None: @@ -184,7 +189,7 @@ def gen_result_wrapper(kls): if kls is set: return str(set(self)) - return cast(str, kls.__str__(self)) + return kls.__str__(self) return self.render_result @@ -212,10 +217,8 @@ class TupleWrapper(tuple, ResultWrapper): return self.render_result -RESULT_WRAPPERS: dict[type, type] = { - kls: gen_result_wrapper(kls) # type: ignore[no-untyped-call] - for kls in (list, dict, set) -} +_types: tuple[type[dict | list | set], ...] = (dict, list, set) +RESULT_WRAPPERS: dict[type, type] = {kls: gen_result_wrapper(kls) for kls in _types} RESULT_WRAPPERS[tuple] = TupleWrapper @@ -329,19 +332,19 @@ class Template: "_hash_cache", ) - def __init__(self, template, hass=None): + def __init__(self, template: str, hass: HomeAssistant | None = None) -> None: """Instantiate a template.""" if not isinstance(template, str): raise TypeError("Expected template to be a string") self.template: str = template.strip() - self._compiled_code = None + self._compiled_code: CodeType | None = None self._compiled: jinja2.Template | None = None self.hass = hass self.is_static = not is_template_string(template) - self._exc_info = None - self._limited = None - self._strict = None + self._exc_info: sys._OptExcInfo | None = None + self._limited: bool | None = None + self._strict: bool | None = None self._hash_cache: int = hash(self.template) @property @@ -366,7 +369,7 @@ class Template: return try: - self._compiled_code = self._env.compile(self.template) # type: ignore[no-untyped-call] + self._compiled_code = self._env.compile(self.template) except jinja2.TemplateError as err: raise TemplateError(err) from err @@ -382,10 +385,10 @@ class Template: If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if not parse_result or self.hass.config.legacy_templates: + if not parse_result or self.hass and self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) - + assert self.hass is not None, "hass variable not set on template" return run_callback_threadsafe( self.hass.loop, partial(self.async_render, variables, parse_result, limited, **kwargs), @@ -407,7 +410,7 @@ class Template: If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if not parse_result or self.hass.config.legacy_templates: + if not parse_result or self.hass and self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) @@ -423,7 +426,7 @@ class Template: render_result = render_result.strip() - if self.hass.config.legacy_templates or not parse_result: + if not parse_result or self.hass and self.hass.config.legacy_templates: return render_result return self._parse_result(render_result) @@ -493,6 +496,7 @@ class Template: finish_event = asyncio.Event() def _render_template() -> None: + assert self.hass is not None, "hass variable not set on template" try: _render_with_context(self.template, compiled, **kwargs) except TimeoutError: @@ -559,8 +563,11 @@ class Template: @callback def async_render_with_possible_json_value( - self, value, error_value=_SENTINEL, variables=None - ): + self, + value: Any, + error_value: Any = _SENTINEL, + variables: dict[str, Any] | None = None, + ) -> Any: """Render template with value exposed. If valid JSON will expose value_json too. @@ -570,8 +577,7 @@ class Template: if self.is_static: return self.template - if self._compiled is None: - self._ensure_compiled() + compiled = self._compiled or self._ensure_compiled() variables = dict(variables or {}) variables["value"] = value @@ -580,9 +586,7 @@ class Template: variables["value_json"] = json_loads(value) try: - return _render_with_context( - self.template, self._compiled, **variables - ).strip() + return _render_with_context(self.template, compiled, **variables).strip() except jinja2.TemplateError as ex: if error_value is _SENTINEL: _LOGGER.error( @@ -607,6 +611,7 @@ class Template: self._strict is None or self._strict == strict ), "can't change between strict and non strict template" assert not (strict and limited), "can't combine strict and limited template" + assert self._compiled_code is not None, "template code was not compiled" self._limited = limited self._strict = strict @@ -683,7 +688,7 @@ class AllStates: if render_info is not None: render_info.all_states_lifecycle = True - def __iter__(self): + def __iter__(self) -> Generator[TemplateState, None, None]: """Return all states.""" self._collect_all() return _state_generator(self._hass, None) @@ -693,7 +698,7 @@ class AllStates: self._collect_all_lifecycle() return self._hass.states.async_entity_ids_count() - def __call__(self, entity_id): + def __call__(self, entity_id: str) -> str: """Return the states.""" state = _get_state(self._hass, entity_id) return STATE_UNKNOWN if state is None else state.state @@ -716,7 +721,7 @@ class DomainStates: self._hass = hass self._domain = domain - def __getattr__(self, name): + def __getattr__(self, name: str) -> TemplateState | None: """Return the states.""" return _get_state_if_valid(self._hass, f"{self._domain}.{name}") @@ -734,7 +739,7 @@ class DomainStates: if entity_collect is not None: entity_collect.domains_lifecycle.add(self._domain) - def __iter__(self): + def __iter__(self) -> Generator[TemplateState, None, None]: """Return the iteration over all the states.""" self._collect_domain() return _state_generator(self._hass, self._domain) @@ -774,7 +779,7 @@ class TemplateStateBase(State): # Jinja will try __getitem__ first and it avoids the need # to call is_safe_attribute - def __getitem__(self, item): + def __getitem__(self, item: str) -> Any: """Return a property as an attribute for jinja.""" if item in _COLLECTABLE_STATE_ATTRIBUTES: # _collect_state inlined here for performance @@ -788,7 +793,7 @@ class TemplateStateBase(State): raise KeyError @property - def entity_id(self): + def entity_id(self) -> str: # type: ignore[override] """Wrap State.entity_id. Intentionally does not collect state @@ -796,49 +801,49 @@ class TemplateStateBase(State): return self._entity_id @property - def state(self): + def state(self) -> str: # type: ignore[override] """Wrap State.state.""" self._collect_state() return self._state.state @property - def attributes(self): + def attributes(self) -> ReadOnlyDict[str, Any]: # type: ignore[override] """Wrap State.attributes.""" self._collect_state() return self._state.attributes @property - def last_changed(self): + def last_changed(self) -> datetime: # type: ignore[override] """Wrap State.last_changed.""" self._collect_state() return self._state.last_changed @property - def last_updated(self): + def last_updated(self) -> datetime: # type: ignore[override] """Wrap State.last_updated.""" self._collect_state() return self._state.last_updated @property - def context(self): + def context(self) -> Context: # type: ignore[override] """Wrap State.context.""" self._collect_state() return self._state.context @property - def domain(self): + def domain(self) -> str: # type: ignore[override] """Wrap State.domain.""" self._collect_state() return self._state.domain @property - def object_id(self): + def object_id(self) -> str: # type: ignore[override] """Wrap State.object_id.""" self._collect_state() return self._state.object_id @property - def name(self): + def name(self) -> str: """Wrap State.name.""" self._collect_state() return self._state.name @@ -882,7 +887,7 @@ class TemplateStateFromEntityId(TemplateStateBase): super().__init__(hass, collect, entity_id) @property - def _state(self) -> State: # type: ignore[override] # mypy issue 4125 + def _state(self) -> State: # type: ignore[override] state = self._hass.states.get(self._entity_id) if not state: state = State(self._entity_id, STATE_UNKNOWN) @@ -903,7 +908,9 @@ def _template_state_no_collect(hass: HomeAssistant, state: State) -> TemplateSta return TemplateState(hass, state, collect=False) -def _state_generator(hass: HomeAssistant, domain: str | None) -> Generator: +def _state_generator( + hass: HomeAssistant, domain: str | None +) -> Generator[TemplateState, None, None]: """State generator for a domain or all states.""" for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")): yield _template_state_no_collect(hass, state) @@ -912,7 +919,7 @@ def _state_generator(hass: HomeAssistant, domain: str | None) -> Generator: def _get_state_if_valid(hass: HomeAssistant, entity_id: str) -> TemplateState | None: state = hass.states.get(entity_id) if state is None and not valid_entity_id(entity_id): - raise TemplateError(f"Invalid entity ID '{entity_id}'") # type: ignore[arg-type] + raise TemplateError(f"Invalid entity ID '{entity_id}'") return _get_template_state_from_state(hass, entity_id, state) @@ -1361,10 +1368,12 @@ def distance(hass, *args): ) -def is_state(hass: HomeAssistant, entity_id: str, state: State) -> bool: +def is_state(hass: HomeAssistant, entity_id: str, state: str | list[str]) -> bool: """Test if a state is a specific value.""" state_obj = _get_state(hass, entity_id) - return state_obj is not None and state_obj.state == state + return state_obj is not None and ( + state_obj.state == state or isinstance(state, list) and state_obj.state in state + ) def is_state_attr(hass: HomeAssistant, entity_id: str, name: str, value: Any) -> bool: @@ -2075,12 +2084,14 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): # evaluated fresh with every execution, rather than executed # at compile time and the value stored. The context itself # can be discarded, we only need to get at the hass object. - def hassfunction(func): + def hassfunction( + func: Callable[Concatenate[HomeAssistant, _P], _R], + ) -> Callable[Concatenate[Any, _P], _R]: """Wrap function that depend on hass.""" @wraps(func) - def wrapper(*args, **kwargs): - return func(hass, *args[1:], **kwargs) + def wrapper(_: Any, *args: _P.args, **kwargs: _P.kwargs) -> _R: + return func(hass, *args, **kwargs) return pass_context(wrapper) @@ -2088,7 +2099,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["device_entities"] = pass_context(self.globals["device_entities"]) self.globals["device_attr"] = hassfunction(device_attr) + self.filters["device_attr"] = pass_context(self.globals["device_attr"]) + self.globals["is_device_attr"] = hassfunction(is_device_attr) + self.tests["is_device_attr"] = pass_eval_context(self.globals["is_device_attr"]) self.globals["config_entry_id"] = hassfunction(config_entry_id) self.filters["config_entry_id"] = pass_context(self.globals["config_entry_id"]) @@ -2177,7 +2191,36 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return super().is_safe_attribute(obj, attr, value) - def compile(self, source, name=None, filename=None, raw=False, defer_init=False): + @overload + def compile( # type: ignore[misc] + self, + source: str | jinja2.nodes.Template, + name: str | None = None, + filename: str | None = None, + raw: Literal[False] = False, + defer_init: bool = False, + ) -> CodeType: + ... + + @overload + def compile( + self, + source: str | jinja2.nodes.Template, + name: str | None = None, + filename: str | None = None, + raw: Literal[True] = ..., + defer_init: bool = False, + ) -> str: + ... + + def compile( + self, + source: str | jinja2.nodes.Template, + name: str | None = None, + filename: str | None = None, + raw: bool = False, + defer_init: bool = False, + ) -> CodeType | str: """Compile the template.""" if ( name is not None @@ -2188,8 +2231,15 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): # If there are any non-default keywords args, we do # not cache. In prodution we currently do not have # any instance of this. - return super().compile(source, name, filename, raw, defer_init) + return super().compile( # type: ignore[no-any-return,call-overload] + source, + name, + filename, + raw, + defer_init, + ) + cached: CodeType | str | None if (cached := self.template_cache.get(source)) is None: cached = self.template_cache[source] = super().compile(source) diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index bc3e7ff3565..3a5fb83395a 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -63,7 +63,7 @@ class TraceElement: """Return dictionary version of this TraceElement.""" result: dict[str, Any] = {"path": self.path, "timestamp": self._timestamp} if self._child_key is not None: - domain, item_id = self._child_key.split(".", 1) + domain, _, item_id = self._child_key.partition(".") result["child_id"] = { "domain": domain, "item_id": item_id, diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 616baeeea92..d1953b2fd00 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -97,10 +97,7 @@ def _merge_resources( # Build response resources: dict[str, dict[str, Any]] = {} for component in components: - if "." not in component: - domain = component - else: - domain = component.split(".", 1)[0] + domain = component.partition(".")[0] domain_resources = resources.setdefault(domain, {}) @@ -148,7 +145,7 @@ async def async_get_component_strings( hass: HomeAssistant, language: str, components: set[str] ) -> dict[str, Any]: """Load translations.""" - domains = list({loaded.split(".")[-1] for loaded in components}) + domains = list({loaded.rpartition(".")[-1] for loaded in components}) integrations: dict[str, Integration] = {} ints_or_excs = await async_get_integrations(hass, domains) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 4cb724a6435..1bf9874988d 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -2,7 +2,9 @@ from __future__ import annotations import asyncio +from collections import defaultdict from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field import functools import logging from typing import TYPE_CHECKING, Any, Protocol, TypedDict, cast @@ -19,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Context, + HassJob, HomeAssistant, callback, is_callback, @@ -38,6 +41,8 @@ _PLATFORM_ALIASES = { "homeassistant": ("event", "numeric_state", "state", "time_pattern", "time"), } +DATA_PLUGGABLE_ACTIONS = "pluggable_actions" + class TriggerActionType(Protocol): """Protocol type for trigger action callback.""" @@ -68,6 +73,116 @@ class TriggerInfo(TypedDict): trigger_data: TriggerData +@dataclass +class PluggableActionsEntry: + """Holder to keep track of all plugs and actions for a given trigger.""" + + plugs: set[PluggableAction] = field(default_factory=set) + actions: dict[ + object, + tuple[ + HassJob[[dict[str, Any], Context | None], Coroutine[Any, Any, None]], + dict[str, Any], + ], + ] = field(default_factory=dict) + + +class PluggableAction: + """A pluggable action handler.""" + + _entry: PluggableActionsEntry | None = None + + def __init__(self, update: CALLBACK_TYPE | None = None) -> None: + """Initialize a pluggable action. + + :param update: callback triggered whenever triggers are attached or removed. + """ + self._update = update + + def __bool__(self) -> bool: + """Return if we have something attached.""" + return bool(self._entry and self._entry.actions) + + @callback + def async_run_update(self) -> None: + """Run update function if one exists.""" + if self._update: + self._update() + + @staticmethod + @callback + def async_get_registry(hass: HomeAssistant) -> dict[tuple, PluggableActionsEntry]: + """Return the pluggable actions registry.""" + if data := hass.data.get(DATA_PLUGGABLE_ACTIONS): + return data # type: ignore[no-any-return] + data = defaultdict(PluggableActionsEntry) + hass.data[DATA_PLUGGABLE_ACTIONS] = data + return data + + @staticmethod + @callback + def async_attach_trigger( + hass: HomeAssistant, + trigger: dict[str, str], + action: TriggerActionType, + variables: dict[str, Any], + ) -> CALLBACK_TYPE: + """Attach an action to a trigger entry. Existing or future plugs registered will be attached.""" + reg = PluggableAction.async_get_registry(hass) + key = tuple(sorted(trigger.items())) + entry = reg[key] + + def _update() -> None: + for plug in entry.plugs: + plug.async_run_update() + + @callback + def _remove() -> None: + """Remove this action attachment, and disconnect all plugs.""" + del entry.actions[_remove] + _update() + if not entry.actions and not entry.plugs: + del reg[key] + + job = HassJob(action) + entry.actions[_remove] = (job, variables) + _update() + + return _remove + + @callback + def async_register( + self, hass: HomeAssistant, trigger: dict[str, str] + ) -> CALLBACK_TYPE: + """Register plug in the global plugs dictionary.""" + + reg = PluggableAction.async_get_registry(hass) + key = tuple(sorted(trigger.items())) + self._entry = reg[key] + self._entry.plugs.add(self) + + @callback + def _remove() -> None: + """Remove plug from registration, and clean up entry if there are no actions or plugs registered.""" + assert self._entry + self._entry.plugs.remove(self) + if not self._entry.actions and not self._entry.plugs: + del reg[key] + self._entry = None + + return _remove + + async def async_run( + self, hass: HomeAssistant, context: Context | None = None + ) -> None: + """Run all actions.""" + assert self._entry + for job, variables in self._entry.actions.values(): + task = hass.async_run_hass_job(job, variables, context) + if task: + await task + + async def _async_get_trigger_platform( hass: HomeAssistant, config: ConfigType ) -> DeviceAutomationTriggerProtocol: diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 768b8040729..205a7848613 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine, Generator from datetime import datetime, timedelta import logging +from random import randint from time import monotonic from typing import Any, Generic, TypeVar import urllib.error @@ -14,7 +15,11 @@ import requests from homeassistant import config_entries from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.util.dt import utcnow from . import entity, event @@ -61,6 +66,12 @@ class DataUpdateCoordinator(Generic[_T]): # when it was already checked during setup. self.data: _T = None # type: ignore[assignment] + # Pick a random microsecond to stagger the refreshes + # and avoid a thundering herd. + self._microsecond = randint( + event.RANDOM_MICROSECOND_MIN, event.RANDOM_MICROSECOND_MAX + ) + self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} self._job = HassJob(self._handle_refresh_interval) self._unsub_refresh: CALLBACK_TYPE | None = None @@ -138,11 +149,17 @@ class DataUpdateCoordinator(Generic[_T]): # We _floor_ utcnow to create a schedule on a rounded second, # minimizing the time between the point and the real activation. # That way we obtain a constant update frequency, - # as long as the update process takes less than a second + # as long as the update process takes less than 500ms + # + # We do not align everything to happen at microsecond 0 + # since it increases the risk of a thundering herd + # when multiple coordinators are scheduled to update at the same time. + # + # https://github.com/home-assistant/core/issues/82231 self._unsub_refresh = event.async_track_point_in_utc_time( self.hass, self._job, - utcnow().replace(microsecond=0) + self.update_interval, + utcnow().replace(microsecond=self._microsecond) + self.update_interval, ) async def _handle_refresh_interval(self, _now: datetime) -> None: @@ -170,7 +187,9 @@ class DataUpdateCoordinator(Generic[_T]): fails. Additionally logging is handled by config entry setup to ensure that multiple retries do not cause log spam. """ - await self._async_refresh(log_failures=False, raise_on_auth_failed=True) + await self._async_refresh( + log_failures=False, raise_on_auth_failed=True, raise_on_entry_error=True + ) if self.last_update_success: return ex = ConfigEntryNotReady() @@ -186,6 +205,7 @@ class DataUpdateCoordinator(Generic[_T]): log_failures: bool = True, raise_on_auth_failed: bool = False, scheduled: bool = False, + raise_on_entry_error: bool = False, ) -> None: """Refresh data.""" if self._unsub_refresh: @@ -237,6 +257,19 @@ class DataUpdateCoordinator(Generic[_T]): self.logger.error("Error fetching %s data: %s", self.name, err) self.last_update_success = False + except ConfigEntryError as err: + self.last_exception = err + if self.last_update_success: + if log_failures: + self.logger.error( + "Config entry setup failed while fetching %s data: %s", + self.name, + err, + ) + self.last_update_success = False + if raise_on_entry_error: + raise + except ConfigEntryAuthFailed as err: auth_failed = True self.last_exception = err diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b83c9c8db01..367eb4f6a8a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,34 +1,36 @@ PyJWT==2.5.0 PyNaCl==1.5.0 aiodiscover==1.4.13 -aiohttp==3.8.1 +aiohttp==3.8.3 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.32.2 +async-upnp-client==0.32.3 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.5 +bleak-retry-connector==2.10.1 bleak==0.19.2 -bluetooth-adapters==0.7.0 -bluetooth-auto-recovery==0.3.6 +bluetooth-adapters==0.12.0 +bluetooth-auto-recovery==0.5.4 +bluetooth-data-tools==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.3 -dbus-fast==1.61.1 +dbus-fast==1.75.0 fnvhash==0.1.0 -hass-nabucasa==0.56.0 -home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221108.0 -httpx==0.23.0 +hass-nabucasa==0.61.0 +home-assistant-bluetooth==1.8.1 +home-assistant-frontend==20221207.0 +httpx==0.23.1 ifaddr==0.1.7 +janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 orjson==3.8.1 paho-mqtt==1.6.1 -pillow==9.2.0 +pillow==9.3.0 pip>=21.0,<22.4 psutil-home-assistant==0.0.1 pyserial==3.5 @@ -86,9 +88,9 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.6.1 -h11==0.12.0 -httpcore==0.15.0 +anyio==3.6.2 +h11==0.14.0 +httpcore==0.16.2 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation @@ -97,9 +99,6 @@ hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env numpy==1.23.2 -# pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead -pytest_asyncio==1000000000.0.0 - # Prevent dependency conflicts between sisyphus-control and aioambient # until upper bounds for sisyphus-control have been updated # https://github.com/jkeljo/sisyphus-control/issues/6 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index bd06cf61e4b..27472a2bbd8 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -7,6 +7,8 @@ import logging import os from typing import Any, cast +import pkg_resources + from .core import HomeAssistant, callback from .exceptions import HomeAssistantError from .helpers.typing import UNDEFINED, UndefinedType @@ -225,6 +227,19 @@ class RequirementsManager: This method is a coroutine. It will raise RequirementsNotFound if an requirement can't be satisfied. """ + if self.hass.config.skip_pip_packages: + skipped_requirements = [ + req + for req in requirements + if pkg_resources.Requirement.parse(req).project_name + in self.hass.config.skip_pip_packages + ] + + for req in skipped_requirements: + _LOGGER.warning("Skipping requirement %s. This may cause issues", req) + + requirements = [r for r in requirements if r not in skipped_requirements] + if not (missing := self._find_missing_requirements(requirements)): return self._raise_for_failed_requirements(name, missing) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 5788a6b155a..51b47e0fe2d 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -36,6 +36,7 @@ class RuntimeConfig: config_dir: str skip_pip: bool = False + skip_pip_packages: list[str] = dataclasses.field(default_factory=list) safe_mode: bool = False verbose: bool = False diff --git a/homeassistant/setup.py b/homeassistant/setup.py index d85e4043505..1172ab00bd7 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -29,11 +29,27 @@ ATTR_COMPONENT = "component" BASE_PLATFORMS = {platform.value for platform in Platform} +# DATA_SETUP is a dict[str, asyncio.Task[bool]], indicating domains which are currently +# being setup or which failed to setup +# - Tasks are added to DATA_SETUP by `async_setup_component`, the key is the domain being setup +# and the Task is the `_async_setup_component` helper. +# - Tasks are removed from DATA_SETUP if setup was successful, that is, the task returned True +DATA_SETUP = "setup_tasks" + +# DATA_SETUP_DONE is a dict [str, asyncio.Event], indicating components which will be setup +# - Events are added to DATA_SETUP_DONE during bootstrap by async_set_domains_to_be_loaded, +# the key is the domain which will be loaded +# - Events are set and removed from DATA_SETUP_DONE when async_setup_component is finished, +# regardless of if the setup was successful or not. DATA_SETUP_DONE = "setup_done" + +# DATA_SETUP_DONE is a dict [str, datetime], indicating when an attempt to setup a component +# started DATA_SETUP_STARTED = "setup_started" + +# DATA_SETUP_TIME is a dict [str, timedelta], indicating how time was spent setting up a component DATA_SETUP_TIME = "setup_time" -DATA_SETUP = "setup_tasks" DATA_DEPS_REQS = "deps_reqs_processed" SLOW_SETUP_WARNING = 10 @@ -44,7 +60,9 @@ SLOW_SETUP_MAX_WAIT = 300 def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str]) -> None: """Set domains that are going to be loaded from the config. - This will allow us to properly handle after_dependencies. + This allow us to: + - Properly handle after_dependencies. + - Keep track of domains which will load but have not yet finished loading """ hass.data[DATA_SETUP_DONE] = {domain: asyncio.Event() for domain in domains} @@ -265,7 +283,7 @@ async def _async_setup_component( await asyncio.sleep(0) await hass.config_entries.flow.async_wait_init_flow_finish(domain) - # Add to components before the async_setup + # Add to components before the entry.async_setup # call to avoid a deadlock when forwarding platforms hass.config.components.add(domain) @@ -433,7 +451,7 @@ def async_get_loaded_integrations(hass: core.HomeAssistant) -> set[str]: if "." not in component: integrations.add(component) continue - domain, platform = component.split(".", 1) + domain, _, platform = component.partition(".") if domain in BASE_PLATFORMS: integrations.add(platform) return integrations @@ -458,10 +476,7 @@ def async_start_setup( time_taken = dt_util.utcnow() - started for unique, domain in unique_components.items(): del setup_started[unique] - if "." in domain: - _, integration = domain.split(".", 1) - else: - integration = domain + integration = domain.rpartition(".")[-1] if integration in setup_time: setup_time[integration] += time_taken else: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 6562ecedb4f..315c8ebda74 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -156,7 +156,7 @@ class Throttle: # be prefixed by '..' so we strip that out. is_func = ( not hasattr(method, "__self__") - and "." not in method.__qualname__.split("..")[-1] + and "." not in method.__qualname__.rpartition("..")[-1] ) @wraps(method) diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py index cb5969b3079..06471eaca6a 100644 --- a/homeassistant/util/file.py +++ b/homeassistant/util/file.py @@ -54,11 +54,10 @@ def write_utf8_file( """ tmp_filename = "" - tmp_path = os.path.split(filename)[0] try: # Modern versions of Python tempfile create this file with mode 0o600 with tempfile.NamedTemporaryFile( - mode="w", encoding="utf-8", dir=tmp_path, delete=False + mode="w", encoding="utf-8", dir=os.path.dirname(filename), delete=False ) as fdesc: fdesc.write(utf8_data) tmp_filename = fdesc.name diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 7e338f8f313..9c6b6045cf9 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -17,6 +17,7 @@ from homeassistant.const import ( WIND_SPEED, UnitOfLength, UnitOfMass, + UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -247,7 +248,7 @@ validate_unit_system = vol.All( METRIC_SYSTEM = UnitSystem( _CONF_UNIT_SYSTEM_METRIC, - accumulated_precipitation=UnitOfLength.MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={ # Convert non-metric distances ("distance", UnitOfLength.FEET): UnitOfLength.METERS, @@ -256,6 +257,8 @@ METRIC_SYSTEM = UnitSystem( ("distance", UnitOfLength.YARDS): UnitOfLength.METERS, # Convert non-metric volumes of gas meters ("gas", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS, + # Convert non-metric precipitation + ("precipitation", UnitOfLength.INCHES): UnitOfLength.MILLIMETERS, # Convert non-metric speeds except knots to km/h ("speed", UnitOfSpeed.FEET_PER_SECOND): UnitOfSpeed.KILOMETERS_PER_HOUR, ("speed", UnitOfSpeed.MILES_PER_HOUR): UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -277,7 +280,7 @@ METRIC_SYSTEM = UnitSystem( US_CUSTOMARY_SYSTEM = UnitSystem( _CONF_UNIT_SYSTEM_US_CUSTOMARY, - accumulated_precipitation=UnitOfLength.INCHES, + accumulated_precipitation=UnitOfPrecipitationDepth.INCHES, conversions={ # Convert non-USCS distances ("distance", UnitOfLength.CENTIMETERS): UnitOfLength.INCHES, @@ -286,6 +289,8 @@ US_CUSTOMARY_SYSTEM = UnitSystem( ("distance", UnitOfLength.MILLIMETERS): UnitOfLength.INCHES, # Convert non-USCS volumes of gas meters ("gas", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET, + # Convert non-USCS precipitation + ("precipitation", UnitOfLength.MILLIMETERS): UnitOfLength.INCHES, # Convert non-USCS speeds except knots to mph ("speed", UnitOfSpeed.METERS_PER_SECOND): UnitOfSpeed.MILES_PER_HOUR, ("speed", UnitOfSpeed.KILOMETERS_PER_HOUR): UnitOfSpeed.MILES_PER_HOUR, diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index 9f69c6c346e..db8b496d90e 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -12,7 +12,9 @@ from .objects import Input, NodeListClass try: from yaml import CSafeDumper as FastestAvailableSafeDumper except ImportError: - from yaml import SafeDumper as FastestAvailableSafeDumper # type: ignore[misc] + from yaml import ( # type: ignore[assignment] + SafeDumper as FastestAvailableSafeDumper, + ) def dump(_dict: dict) -> str: diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 09e19af6840..62d754329c4 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -18,7 +18,7 @@ try: HAS_C_LOADER = True except ImportError: HAS_C_LOADER = False - from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[misc] + from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[assignment] from homeassistant.exceptions import HomeAssistantError diff --git a/mypy.ini b/mypy.ini index cd6bc14169d..5a6615d0f48 100644 --- a/mypy.ini +++ b/mypy.ini @@ -14,6 +14,7 @@ warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true enable_error_code = ignore-without-code +disable_error_code = annotation-unchecked strict_concatenate = false check_untyped_defs = true disallow_incomplete_defs = true @@ -27,28 +28,16 @@ warn_unreachable = true [mypy-homeassistant.*] no_implicit_reexport = true -[mypy-homeassistant.exceptions] +[mypy-homeassistant.auth.auth_store] +disallow_any_generics = true + +[mypy-homeassistant.auth.providers.*] disallow_any_generics = true [mypy-homeassistant.core] disallow_any_generics = true -[mypy-homeassistant.loader] -disallow_any_generics = true - -[mypy-homeassistant.requirements] -disallow_any_generics = true - -[mypy-homeassistant.runner] -disallow_any_generics = true - -[mypy-homeassistant.setup] -disallow_any_generics = true - -[mypy-homeassistant.auth.auth_store] -disallow_any_generics = true - -[mypy-homeassistant.auth.providers.*] +[mypy-homeassistant.exceptions] disallow_any_generics = true [mypy-homeassistant.helpers.area_registry] @@ -99,6 +88,18 @@ disallow_any_generics = true [mypy-homeassistant.helpers.translation] disallow_any_generics = true +[mypy-homeassistant.loader] +disallow_any_generics = true + +[mypy-homeassistant.requirements] +disallow_any_generics = true + +[mypy-homeassistant.runner] +disallow_any_generics = true + +[mypy-homeassistant.setup] +disallow_any_generics = true + [mypy-homeassistant.util.async_] disallow_any_generics = true @@ -542,16 +543,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.cover.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.clickatell.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -572,6 +563,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.cover.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.cpuspeed.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -612,6 +613,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.derivative.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.device_automation.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1262,6 +1273,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.image.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.image_processing.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1482,6 +1503,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.logger.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lookin.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1512,6 +1543,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.matter.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.media_player.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1552,6 +1593,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.min_max.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1592,6 +1643,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.mqtt.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mysensors.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1662,6 +1723,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.nextdns.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.nfandroidtv.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1882,6 +1953,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.radarr.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rainmachine.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1902,16 +1983,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.radarr.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.recollect_waste.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2032,6 +2103,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ruuvitag_ble.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.samsungtv.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2092,6 +2173,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.sensirion_ble.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.sensor.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2584,6 +2675,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.wake_on_lan.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.wallbox.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 6562785180d..a60b1c7de85 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -661,7 +661,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="supported_features", - return_type="int", + return_type="AlarmControlPanelEntityFeature", ), TypeHintMatch( function_name="alarm_disarm", @@ -803,7 +803,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="supported_features", - return_type="int", + return_type="CameraEntityFeature", ), TypeHintMatch( function_name="is_recording", @@ -1055,7 +1055,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="supported_features", - return_type="int", + return_type="ClimateEntityFeature", ), TypeHintMatch( function_name="min_temp", @@ -1108,6 +1108,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="is_closed", return_type=["bool", None], ), + TypeHintMatch( + function_name="supported_features", + return_type="CoverEntityFeature", + ), TypeHintMatch( function_name="open_cover", kwargs_type="Any", @@ -1284,6 +1288,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="preset_modes", return_type=["list[str]", None], ), + TypeHintMatch( + function_name="supported_features", + return_type="FanEntityFeature", + ), TypeHintMatch( function_name="set_percentage", arg_types={1: "int"}, @@ -1387,6 +1395,61 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "humidifier": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="HumidifierEntity", + matches=[ + TypeHintMatch( + function_name="available_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="device_class", + return_type=["HumidifierDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="min_humidity", + return_type=["int"], + ), + TypeHintMatch( + function_name="max_humidity", + return_type=["int"], + ), + TypeHintMatch( + function_name="mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="supported_features", + return_type="HumidifierEntityFeature", + ), + TypeHintMatch( + function_name="target_humidity", + return_type=["int", None], + ), + TypeHintMatch( + function_name="set_humidity", + arg_types={1: "str"}, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_mode", + arg_types={1: "str"}, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "light": [ ClassTypeHintMatch( base_class="Entity", @@ -1457,7 +1520,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="supported_features", - return_type="int", + return_type="LightEntityFeature", ), TypeHintMatch( function_name="turn_on", @@ -1518,6 +1581,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="is_jammed", return_type=["bool", None], ), + TypeHintMatch( + function_name="supported_features", + return_type="LockEntityFeature", + ), TypeHintMatch( function_name="lock", kwargs_type="Any", @@ -1721,6 +1788,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="group_members", return_type=["list[str]", None], ), + TypeHintMatch( + function_name="supported_features", + return_type="MediaPlayerEntityFeature", + ), TypeHintMatch( function_name="turn_on", return_type=None, @@ -1981,16 +2052,16 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="RemoteEntity", matches=[ TypeHintMatch( - function_name="supported_features", - return_type="int", + function_name="activity_list", + return_type=["list[str]", None], ), TypeHintMatch( function_name="current_activity", return_type=["str", None], ), TypeHintMatch( - function_name="activity_list", - return_type=["list[str]", None], + function_name="supported_features", + return_type="RemoteEntityFeature", ), TypeHintMatch( function_name="send_command", @@ -2141,6 +2212,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="available_tones", return_type=["dict[int, str]", "list[int | str]", None], ), + TypeHintMatch( + function_name="supported_features", + return_type="SirenEntityFeature", + ), ], ), ], @@ -2270,7 +2345,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="supported_features", - return_type="int", + return_type="UpdateEntityFeature", ), TypeHintMatch( function_name="title", @@ -2319,6 +2394,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="fan_speed_list", return_type="list[str]", ), + TypeHintMatch( + function_name="supported_features", + return_type="VacuumEntityFeature", + ), TypeHintMatch( function_name="stop", kwargs_type="Any", @@ -2431,24 +2510,36 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="WaterHeaterEntity", matches=[ TypeHintMatch( - function_name="precision", + function_name="current_operation", + return_type=["str", None], + ), + TypeHintMatch( + function_name="current_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="is_away_mode_on", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="max_temp", return_type="float", ), TypeHintMatch( - function_name="temperature_unit", - return_type="str", - ), - TypeHintMatch( - function_name="current_operation", - return_type=["str", None], + function_name="min_temp", + return_type="float", ), TypeHintMatch( function_name="operation_list", return_type=["list[str]", None], ), TypeHintMatch( - function_name="current_temperature", - return_type=["float", None], + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="supported_features", + return_type="WaterHeaterEntityFeature", ), TypeHintMatch( function_name="target_temperature", @@ -2463,8 +2554,8 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { return_type=["float", None], ), TypeHintMatch( - function_name="is_away_mode_on", - return_type=["bool", None], + function_name="temperature_unit", + return_type="str", ), TypeHintMatch( function_name="set_temperature", @@ -2488,14 +2579,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { return_type=None, has_async_counterpart=True, ), - TypeHintMatch( - function_name="min_temp", - return_type="float", - ), - TypeHintMatch( - function_name="max_temp", - return_type="float", - ), ], ), ], diff --git a/pyproject.toml b/pyproject.toml index 2a796e8895e..3c65b01d26d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.5" +version = "2022.12.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -24,7 +24,7 @@ classifiers = [ ] requires-python = ">=3.9.0" dependencies = [ - "aiohttp==3.8.1", + "aiohttp==3.8.3", "astral==2.2", "async_timeout==4.0.2", "attrs==21.2.0", @@ -35,8 +35,8 @@ dependencies = [ "ciso8601==2.2.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.23.0", - "home-assistant-bluetooth==1.6.0", + "httpx==0.23.1", + "home-assistant-bluetooth==1.8.1", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", @@ -227,3 +227,4 @@ norecursedirs = [ ] log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_mode = "auto" diff --git a/requirements.txt b/requirements.txt index 96a9f801df9..6c878206b07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.8.1 +aiohttp==3.8.3 astral==2.2 async_timeout==4.0.2 attrs==21.2.0 @@ -10,8 +10,8 @@ awesomeversion==22.9.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 -httpx==0.23.0 -home-assistant-bluetooth==1.6.0 +httpx==0.23.1 +home-assistant-bluetooth==1.8.1 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 diff --git a/requirements_all.txt b/requirements_all.txt index f299e5b6d32..58decaa1158 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.47 +AIOAladdinConnect==0.1.48 # homeassistant.components.adax Adax-local==0.1.5 @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.5 +PySwitchbot==0.22.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -112,8 +112,11 @@ aio_geojson_usgs_earthquakes==0.1 # homeassistant.components.gdacs aio_georss_gdacs==0.7 +# homeassistant.components.airq +aioairq==0.2.4 + # homeassistant.components.airzone -aioairzone==0.4.8 +aioairzone==0.5.1 # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -147,13 +150,13 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.09.3 +aioecowitt==2022.11.0 # homeassistant.components.emonitor aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.3 +aioesphomeapi==13.0.1 # homeassistant.components.flo aioflo==2021.11.0 @@ -171,7 +174,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.19 +aiohomekit==2.4.1 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -190,7 +193,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.8.6 +aiolifx==0.8.7 # homeassistant.components.lifx aiolifx_effects==0.3.0 @@ -198,11 +201,14 @@ aiolifx_effects==0.3.0 # homeassistant.components.lifx aiolifx_themes==0.2.0 +# homeassistant.components.livisi +aiolivisi==0.0.14 + # homeassistant.components.lookin aiolookin==0.1.1 # homeassistant.components.lyric -aiolyric==1.0.8 +aiolyric==1.0.9 # homeassistant.components.modern_forms aiomodernforms==0.1.8 @@ -229,7 +235,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.3 +aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 @@ -255,7 +261,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.2 +aioshelly==5.1.0 # homeassistant.components.skybell aioskybell==22.7.0 @@ -267,16 +273,16 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.1.0 +aioswitcher==3.2.1 # homeassistant.components.syncthing aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.4 +aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==41 +aiounifi==42 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -327,7 +333,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.1.0 +apprise==1.2.0 # homeassistant.components.aprs aprslib==0.7.0 @@ -335,8 +341,11 @@ aprslib==0.7.0 # homeassistant.components.aqualogic aqualogic==2.6 +# homeassistant.components.aranet +aranet4==2.1.3 + # homeassistant.components.arcam_fmj -arcam-fmj==0.12.0 +arcam-fmj==1.0.1 # homeassistant.components.arris_tg2492lg arris-tg2492lg==1.2.1 @@ -353,7 +362,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.2 +async-upnp-client==0.32.3 # homeassistant.components.supla asyncpysupla==0.0.5 @@ -404,7 +413,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.4 +bellows==0.34.5 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -413,7 +422,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.5 +bleak-retry-connector==2.10.1 # homeassistant.components.bluetooth bleak==0.19.2 @@ -438,10 +447,14 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.7.0 +bluetooth-adapters==0.12.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.6 +bluetooth-auto-recovery==0.5.4 + +# homeassistant.components.bluetooth +# homeassistant.components.led_ble +bluetooth-data-tools==0.3.0 # homeassistant.components.bond bond-async==0.1.22 @@ -454,7 +467,7 @@ boschshcpy==0.2.35 boto3==1.20.24 # homeassistant.components.broadlink -broadlink==0.18.2 +broadlink==0.18.3 # homeassistant.components.brother brother==2.0.0 @@ -469,7 +482,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==1.2.2 +bthome-ble==2.3.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 @@ -540,7 +553,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.61.1 +dbus-fast==1.75.0 # homeassistant.components.debugpy debugpy==1.6.3 @@ -596,7 +609,7 @@ dwdwfsapi==1.0.5 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.46 +dynalite_devices==0.1.47 # homeassistant.components.rainforest_eagle eagle100==0.1.1 @@ -690,7 +703,7 @@ fixerio==1.0.0a0 fjaraskupan==2.2.0 # homeassistant.components.flipr -flipr-api==1.4.2 +flipr-api==1.4.4 # homeassistant.components.flux_led flux_led==0.28.34 @@ -725,7 +738,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.3 +gcal-sync==4.0.4 # homeassistant.components.geniushub geniushub-client==0.6.30 @@ -734,7 +747,7 @@ geniushub-client==0.6.30 geocachingapi==0.2.1 # homeassistant.components.aprs -geopy==2.1.0 +geopy==2.3.0 # homeassistant.components.geo_rss_events georss_generic_client==0.6 @@ -774,7 +787,7 @@ google-cloud-pubsub==2.13.10 google-cloud-texttospeech==2.12.3 # homeassistant.components.nest -google-nest-sdm==2.0.0 +google-nest-sdm==2.1.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -804,7 +817,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.4 +growattServer==1.3.0 # homeassistant.components.google_sheets gspread==5.5.0 @@ -834,11 +847,8 @@ ha-philipsjs==2.9.0 # homeassistant.components.habitica habitipy==0.2.0 -# homeassistant.components.hangouts -hangups==0.4.18 - # homeassistant.components.cloud -hass-nabucasa==0.56.0 +hass-nabucasa==0.61.0 # homeassistant.components.splunk hass_splunk==0.1.1 @@ -853,7 +863,10 @@ hdate==0.10.4 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -herepy==2.0.0 +here_routing==0.1.1 + +# homeassistant.components.here_travel_time +here_transit==1.0.0 # homeassistant.components.hikvisioncam hikvision==0.4 @@ -868,16 +881,16 @@ hlk-sw16==0.0.9 hole==0.7.0 # homeassistant.components.workday -holidays==0.16 +holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221108.0 +home-assistant-frontend==20221207.0 # homeassistant.components.home_connect homeconnect==0.7.2 # homeassistant.components.homematicip_cloud -homematicip==1.0.7 +homematicip==1.0.11 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 @@ -912,6 +925,9 @@ ibm-watson==5.2.2 # homeassistant.components.watson_iot ibmiotf==0.3.4 +# homeassistant.components.local_calendar +ical==4.2.1 + # homeassistant.components.ping icmplib==3.0 @@ -951,6 +967,9 @@ iperf3==0.1.11 # homeassistant.components.gogogate2 ismartgate==4.0.4 +# homeassistant.components.file_upload +janus==1.0.0 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 @@ -1030,10 +1049,10 @@ logi_circle==0.2.3 london-tube-status==0.5 # homeassistant.components.luftdaten -luftdaten==0.7.2 +luftdaten==0.7.4 # homeassistant.components.lupusec -lupupy==0.1.9 +lupupy==0.2.1 # homeassistant.components.lw12wifi lw12==0.9.2 @@ -1042,7 +1061,7 @@ lw12==0.9.2 lxml==4.9.1 # homeassistant.components.nmap_tracker -mac-vendor-lookup==0.1.11 +mac-vendor-lookup==0.1.12 # homeassistant.components.magicseaweed magicseaweed==1.0.3 @@ -1132,7 +1151,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.4.2 +nettigo-air-monitor==1.5.0 # homeassistant.components.neurio_energy neurio==0.3.1 @@ -1147,10 +1166,10 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.1.1 +nextdns==1.2.2 # homeassistant.components.nibe_heatpump -nibe==0.5.0 +nibe==1.3.0 # homeassistant.components.niko_home_control niko-home-control==0.2.1 @@ -1174,7 +1193,7 @@ nsapi==3.0.5 nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat -nuheat==0.3.0 +nuheat==1.0.0 # homeassistant.components.numato numato-gpio==0.10.0 @@ -1182,6 +1201,7 @@ numato-gpio==0.10.0 # homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv +# homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend numpy==1.23.2 @@ -1241,7 +1261,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.14.2 +oralb-ble==0.14.3 # homeassistant.components.oru oru==0.1.11 @@ -1300,13 +1320,13 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.2.0 +pillow==9.3.0 # homeassistant.components.dominos pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.13.0 +plexapi==4.13.1 # homeassistant.components.plex plexauth==0.0.6 @@ -1315,7 +1335,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.7 +plugwise==0.25.14 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1348,7 +1368,7 @@ proxmoxer==1.3.1 psutil-home-assistant==0.0.1 # homeassistant.components.systemmonitor -psutil==5.9.3 +psutil==5.9.4 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 @@ -1412,7 +1432,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.1 +pyTibber==0.26.4 # homeassistant.components.dlink pyW215==0.7.0 @@ -1478,13 +1498,13 @@ pybravia==0.2.3 pycarwings2==2.13 # homeassistant.components.cloudflare -pycfdns==1.2.2 +pycfdns==2.0.1 # homeassistant.components.channels pychannels==1.2.3 # homeassistant.components.cast -pychromecast==12.1.4 +pychromecast==13.0.1 # homeassistant.components.pocketcasts pycketcasts==1.0.1 @@ -1502,7 +1522,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.microsoft -pycsspeechtts==1.0.4 +pycsspeechtts==1.0.8 # homeassistant.components.cups # pycups==1.9.73 @@ -1607,7 +1627,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hikvision -pyhik==0.3.0 +pyhik==0.3.1 # homeassistant.components.hive pyhiveapi==0.5.14 @@ -1727,7 +1747,7 @@ pymochad==0.2.0 pymodbus==2.5.3 # homeassistant.components.monoprice -pymonoprice==0.3 +pymonoprice==0.4 # homeassistant.components.msteams pymsteams==0.1.12 @@ -1751,7 +1771,7 @@ pynina==0.1.8 pynobo==1.6.0 # homeassistant.components.nuki -pynuki==1.5.2 +pynuki==1.6.0 # homeassistant.components.nut pynut2==2.1.2 @@ -1792,7 +1812,7 @@ pyotgw==2.1.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.6 +pyoverkiz==1.7.1 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1837,7 +1857,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.4.3 +pyrainbird==0.6.3 # homeassistant.components.recswitch pyrecswitch==1.0.2 @@ -1846,7 +1866,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.5 +pyrisco==0.5.6 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1932,7 +1952,7 @@ pystiebeleltron==0.0.1.dev2 pysuez==0.1.19 # homeassistant.components.switchbee -pyswitchbee==1.5.5 +pyswitchbee==1.6.1 # homeassistant.components.syncthru pysyncthru==0.7.10 @@ -1953,7 +1973,7 @@ pythinkingcleaner==0.0.3 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.5 +python-bsblan==0.5.8 # homeassistant.components.clementine python-clementine-remote==1.0.1 @@ -1989,7 +2009,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.1.0 +python-homewizard-energy==1.3.1 # homeassistant.components.hp_ilo python-hpilo==4.3 @@ -2009,6 +2029,9 @@ python-kasa==0.5.0 # homeassistant.components.lirc # python-lirc==1.2.3 +# homeassistant.components.matter +python-matter-server==1.0.6 + # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -2072,13 +2095,13 @@ pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.1 +pytrafikverket==0.2.2 # homeassistant.components.usb pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.3.4 +pyunifiprotect==4.5.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2093,7 +2116,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==2.0.3 +pyvesync==2.1.1 # homeassistant.components.vizio pyvizio==0.1.57 @@ -2203,6 +2226,9 @@ russound==0.1.9 # homeassistant.components.russound_rio russound_rio==0.1.8 +# homeassistant.components.ruuvitag_ble +ruuvitag-ble==0.1.1 + # homeassistant.components.yamaha rxv==0.7.0 @@ -2235,7 +2261,10 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.4 +sense_energy==0.11.0 + +# homeassistant.components.sensirion_ble +sensirion-ble==0.0.1 # homeassistant.components.sensorpro sensorpro-ble==0.5.0 @@ -2244,7 +2273,7 @@ sensorpro-ble==0.5.0 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.10.0 +sentry-sdk==1.11.0 # homeassistant.components.sharkiq sharkiq==0.0.1 @@ -2310,7 +2339,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.20.0 +spotipy==2.21.0 # homeassistant.components.recorder # homeassistant.components.sql @@ -2344,7 +2373,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.6.1 +subarulink==0.7.0 # homeassistant.components.solarlog sunwatcher==0.2.1 @@ -2385,9 +2414,6 @@ temescal==0.5 # homeassistant.components.temper temperusb==1.6.0 -# homeassistant.components.nibe_heatpump -tenacity==8.0.1 - # homeassistant.components.tensorflow # tensorflow==2.5.0 @@ -2401,7 +2427,7 @@ tesla-wall-connector==1.0.2 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.2 +thermobeacon-ble==0.4.0 # homeassistant.components.thermopro thermopro-ble==0.4.3 @@ -2440,7 +2466,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.3 +ttls==1.5.1 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 @@ -2458,7 +2484,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.5.0 +ultraheat-api==0.5.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 @@ -2518,7 +2544,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.10 +wallbox==0.4.12 # homeassistant.components.waqi waqiasync==1.0.0 @@ -2560,10 +2586,10 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.10.0 +xiaomi-ble==0.12.2 # homeassistant.components.knx -xknx==1.2.1 +xknx==2.1.0 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -2580,11 +2606,14 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.5 +yalexs-ble==1.10.2 # homeassistant.components.august yalexs==1.2.6 +# homeassistant.components.august +yalexs_ble==1.10.2 + # homeassistant.components.yeelight yeelight==0.7.10 @@ -2592,7 +2621,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.1.0 +yolink-api==0.1.5 # homeassistant.components.youless youless-api==0.16 @@ -2610,7 +2639,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.87 +zha-quirks==0.0.88 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2619,7 +2648,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.19.1 +zigpy-deconz==0.19.2 # homeassistant.components.zha zigpy-xbee==0.16.2 @@ -2628,16 +2657,16 @@ zigpy-xbee==0.16.2 zigpy-zigate==0.10.3 # homeassistant.components.zha -zigpy-znp==0.9.1 +zigpy-znp==0.9.2 # homeassistant.components.zha -zigpy==0.51.6 +zigpy==0.52.3 # homeassistant.components.zoneminder zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.43.0 +zwave-js-server-python==0.43.1 # homeassistant.components.zwave_me -zwave_me_ws==0.2.6 +zwave_me_ws==0.3.0 diff --git a/requirements_test.txt b/requirements_test.txt index b15ceb3b002..2bed839649c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,16 +7,17 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -astroid==2.12.12 +astroid==2.12.13 codecov==2.1.12 coverage==6.4.4 freezegun==1.2.2 mock-open==1.4.0 -mypy==0.982 +mypy==0.991 pre-commit==2.20.0 -pylint==2.15.5 +pylint==2.15.7 pipdeptree==2.3.1 -pytest-aiohttp==0.3.0 +pytest-asyncio==0.20.2 +pytest-aiohttp==1.0.4 pytest-cov==3.0.0 pytest-freezegun==0.4.2 pytest-socket==0.5.1 @@ -24,9 +25,9 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-xdist==2.5.0 -pytest==7.1.3 +pytest==7.2.0 requests_mock==1.10.0 -respx==0.19.2 +respx==0.20.1 stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" tqdm==4.64.0 @@ -39,6 +40,7 @@ types-decorator==0.1.7 types-enum34==0.1.8 types-ipaddress==0.1.5 types-pkg-resources==0.1.3 +types-python-dateutil==2.8.19.2 types-python-slugify==0.1.2 types-pytz==2021.1.2 types-PyYAML==5.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85f05c514b3..4f1a6bc31bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.47 +AIOAladdinConnect==0.1.48 # homeassistant.components.adax Adax-local==0.1.5 @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.5 +PySwitchbot==0.22.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -99,8 +99,11 @@ aio_geojson_usgs_earthquakes==0.1 # homeassistant.components.gdacs aio_georss_gdacs==0.7 +# homeassistant.components.airq +aioairq==0.2.4 + # homeassistant.components.airzone -aioairzone==0.4.8 +aioairzone==0.5.1 # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -134,13 +137,13 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.09.3 +aioecowitt==2022.11.0 # homeassistant.components.emonitor aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.3 +aioesphomeapi==13.0.1 # homeassistant.components.flo aioflo==2021.11.0 @@ -155,7 +158,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.19 +aiohomekit==2.4.1 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -168,7 +171,7 @@ aiohue==4.5.0 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx==0.8.6 +aiolifx==0.8.7 # homeassistant.components.lifx aiolifx_effects==0.3.0 @@ -176,11 +179,14 @@ aiolifx_effects==0.3.0 # homeassistant.components.lifx aiolifx_themes==0.2.0 +# homeassistant.components.livisi +aiolivisi==0.0.14 + # homeassistant.components.lookin aiolookin==0.1.1 # homeassistant.components.lyric -aiolyric==1.0.8 +aiolyric==1.0.9 # homeassistant.components.modern_forms aiomodernforms==0.1.8 @@ -204,7 +210,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.3 +aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 @@ -230,7 +236,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.2 +aioshelly==5.1.0 # homeassistant.components.skybell aioskybell==22.7.0 @@ -242,16 +248,16 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.1.0 +aioswitcher==3.2.1 # homeassistant.components.syncthing aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.4 +aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==41 +aiounifi==42 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -293,13 +299,16 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.1.0 +apprise==1.2.0 # homeassistant.components.aprs aprslib==0.7.0 +# homeassistant.components.aranet +aranet4==2.1.3 + # homeassistant.components.arcam_fmj -arcam-fmj==0.12.0 +arcam-fmj==1.0.1 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms @@ -307,7 +316,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.2 +async-upnp-client==0.32.3 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 @@ -331,13 +340,13 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.4 +bellows==0.34.5 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.5 +bleak-retry-connector==2.10.1 # homeassistant.components.bluetooth bleak==0.19.2 @@ -352,10 +361,14 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.7.0 +bluetooth-adapters==0.12.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.6 +bluetooth-auto-recovery==0.5.4 + +# homeassistant.components.bluetooth +# homeassistant.components.led_ble +bluetooth-data-tools==0.3.0 # homeassistant.components.bond bond-async==0.1.22 @@ -364,7 +377,7 @@ bond-async==0.1.22 boschshcpy==0.2.35 # homeassistant.components.broadlink -broadlink==0.18.2 +broadlink==0.18.3 # homeassistant.components.brother brother==2.0.0 @@ -373,7 +386,7 @@ brother==2.0.0 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==1.2.2 +bthome-ble==2.3.1 # homeassistant.components.buienradar buienradar==1.0.5 @@ -420,7 +433,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.61.1 +dbus-fast==1.75.0 # homeassistant.components.debugpy debugpy==1.6.3 @@ -458,7 +471,7 @@ doorbirdpy==2.1.0 dsmr_parser==0.33 # homeassistant.components.dynalite -dynalite_devices==0.1.46 +dynalite_devices==0.1.47 # homeassistant.components.rainforest_eagle eagle100==0.1.1 @@ -512,7 +525,7 @@ fivem-api==0.1.2 fjaraskupan==2.2.0 # homeassistant.components.flipr -flipr-api==1.4.2 +flipr-api==1.4.4 # homeassistant.components.flux_led flux_led==0.28.34 @@ -541,13 +554,13 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.3 +gcal-sync==4.0.4 # homeassistant.components.geocaching geocachingapi==0.2.1 # homeassistant.components.aprs -geopy==2.1.0 +geopy==2.3.0 # homeassistant.components.geo_rss_events georss_generic_client==0.6 @@ -581,7 +594,7 @@ goodwe==0.2.18 google-cloud-pubsub==2.13.10 # homeassistant.components.nest -google-nest-sdm==2.0.0 +google-nest-sdm==2.1.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -599,7 +612,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.4 +growattServer==1.3.0 # homeassistant.components.google_sheets gspread==5.5.0 @@ -626,11 +639,8 @@ ha-philipsjs==2.9.0 # homeassistant.components.habitica habitipy==0.2.0 -# homeassistant.components.hangouts -hangups==0.4.18 - # homeassistant.components.cloud -hass-nabucasa==0.56.0 +hass-nabucasa==0.61.0 # homeassistant.components.tasmota hatasmota==0.6.1 @@ -639,7 +649,10 @@ hatasmota==0.6.1 hdate==0.10.4 # homeassistant.components.here_travel_time -herepy==2.0.0 +here_routing==0.1.1 + +# homeassistant.components.here_travel_time +here_transit==1.0.0 # homeassistant.components.hlk_sw16 hlk-sw16==0.0.9 @@ -648,16 +661,16 @@ hlk-sw16==0.0.9 hole==0.7.0 # homeassistant.components.workday -holidays==0.16 +holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221108.0 +home-assistant-frontend==20221207.0 # homeassistant.components.home_connect homeconnect==0.7.2 # homeassistant.components.homematicip_cloud -homematicip==1.0.7 +homematicip==1.0.11 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 @@ -677,6 +690,9 @@ iaqualink==0.5.0 # homeassistant.components.ibeacon ibeacon_ble==1.0.1 +# homeassistant.components.local_calendar +ical==4.2.1 + # homeassistant.components.ping icmplib==3.0 @@ -704,6 +720,9 @@ iotawattpy==0.1.0 # homeassistant.components.gogogate2 ismartgate==4.0.4 +# homeassistant.components.file_upload +janus==1.0.0 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 @@ -750,13 +769,13 @@ life360==5.3.0 logi_circle==0.2.3 # homeassistant.components.luftdaten -luftdaten==0.7.2 +luftdaten==0.7.4 # homeassistant.components.scrape lxml==4.9.1 # homeassistant.components.nmap_tracker -mac-vendor-lookup==0.1.11 +mac-vendor-lookup==0.1.12 # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -825,7 +844,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.4.2 +nettigo-air-monitor==1.5.0 # homeassistant.components.nexia nexia==2.0.6 @@ -834,10 +853,10 @@ nexia==2.0.6 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.1.1 +nextdns==1.2.2 # homeassistant.components.nibe_heatpump -nibe==0.5.0 +nibe==1.3.0 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 @@ -849,7 +868,7 @@ notify-events==1.0.4 nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat -nuheat==0.3.0 +nuheat==1.0.0 # homeassistant.components.numato numato-gpio==0.10.0 @@ -857,6 +876,7 @@ numato-gpio==0.10.0 # homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv +# homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend numpy==1.23.2 @@ -886,7 +906,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.14.2 +oralb-ble==0.14.3 # homeassistant.components.ovo_energy ovoenergy==1.2.0 @@ -930,10 +950,10 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.2.0 +pillow==9.3.0 # homeassistant.components.plex -plexapi==4.13.0 +plexapi==4.13.1 # homeassistant.components.plex plexauth==0.0.6 @@ -942,7 +962,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.7 +plugwise==0.25.14 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1012,7 +1032,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.1 +pyTibber==0.26.4 # homeassistant.components.nextbus py_nextbusnext==0.1.5 @@ -1054,10 +1074,10 @@ pybotvac==0.0.23 pybravia==0.2.3 # homeassistant.components.cloudflare -pycfdns==1.2.2 +pycfdns==2.0.1 # homeassistant.components.cast -pychromecast==12.1.4 +pychromecast==13.0.1 # homeassistant.components.comfoconnect pycomfoconnect==0.4 @@ -1219,7 +1239,7 @@ pymochad==0.2.0 pymodbus==2.5.3 # homeassistant.components.monoprice -pymonoprice==0.3 +pymonoprice==0.4 # homeassistant.components.myq pymyq==3.1.4 @@ -1237,7 +1257,7 @@ pynina==0.1.8 pynobo==1.6.0 # homeassistant.components.nuki -pynuki==1.5.2 +pynuki==1.6.0 # homeassistant.components.nut pynut2==2.1.2 @@ -1269,7 +1289,7 @@ pyotgw==2.1.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.6 +pyoverkiz==1.7.1 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1302,7 +1322,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.5 +pyrisco==0.5.6 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1361,7 +1381,7 @@ pyspcwebgw==0.4.0 pysqueezebox==0.6.1 # homeassistant.components.switchbee -pyswitchbee==1.5.5 +pyswitchbee==1.6.1 # homeassistant.components.syncthru pysyncthru==0.7.10 @@ -1373,7 +1393,7 @@ pytankerkoenig==0.0.6 pytautulli==21.11.0 # homeassistant.components.bsblan -python-bsblan==0.5.5 +python-bsblan==0.5.8 # homeassistant.components.ecobee python-ecobee-api==0.2.14 @@ -1385,7 +1405,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.11 # homeassistant.components.homewizard -python-homewizard-energy==1.1.0 +python-homewizard-energy==1.3.1 # homeassistant.components.izone python-izone==1.2.9 @@ -1396,6 +1416,9 @@ python-juicenet==1.1.0 # homeassistant.components.tplink python-kasa==0.5.0 +# homeassistant.components.matter +python-matter-server==1.0.6 + # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -1435,13 +1458,13 @@ pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.1 +pytrafikverket==0.2.2 # homeassistant.components.usb pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.3.4 +pyunifiprotect==4.5.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -1450,7 +1473,7 @@ pyuptimerobot==22.2.0 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==2.0.3 +pyvesync==2.1.1 # homeassistant.components.vizio pyvizio==0.1.57 @@ -1518,6 +1541,9 @@ rpi-bad-power==0.1.0 # homeassistant.components.rtsp_to_webrtc rtsp-to-webrtc==0.5.1 +# homeassistant.components.ruuvitag_ble +ruuvitag-ble==0.1.1 + # homeassistant.components.yamaha rxv==0.7.0 @@ -1538,7 +1564,10 @@ securetar==2022.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.4 +sense_energy==0.11.0 + +# homeassistant.components.sensirion_ble +sensirion-ble==0.0.1 # homeassistant.components.sensorpro sensorpro-ble==0.5.0 @@ -1547,7 +1576,7 @@ sensorpro-ble==0.5.0 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.10.0 +sentry-sdk==1.11.0 # homeassistant.components.sharkiq sharkiq==0.0.1 @@ -1595,7 +1624,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.20.0 +spotipy==2.21.0 # homeassistant.components.recorder # homeassistant.components.sql @@ -1623,7 +1652,7 @@ stookalert==0.1.4 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.6.1 +subarulink==0.7.0 # homeassistant.components.solarlog sunwatcher==0.2.1 @@ -1643,9 +1672,6 @@ tellduslive==0.10.11 # homeassistant.components.lg_soundbar temescal==0.5 -# homeassistant.components.nibe_heatpump -tenacity==8.0.1 - # homeassistant.components.powerwall tesla-powerwall==0.3.18 @@ -1653,7 +1679,7 @@ tesla-powerwall==0.3.18 tesla-wall-connector==1.0.2 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.2 +thermobeacon-ble==0.4.0 # homeassistant.components.thermopro thermopro-ble==0.4.3 @@ -1677,7 +1703,7 @@ total_connect_client==2022.10 transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.3 +ttls==1.5.1 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 @@ -1695,7 +1721,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.5.0 +ultraheat-api==0.5.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 @@ -1746,7 +1772,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.10 +wallbox==0.4.12 # homeassistant.components.folder_watcher watchdog==2.1.9 @@ -1773,10 +1799,10 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.10.0 +xiaomi-ble==0.12.2 # homeassistant.components.knx -xknx==1.2.1 +xknx==2.1.0 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -1790,16 +1816,19 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.5 +yalexs-ble==1.10.2 # homeassistant.components.august yalexs==1.2.6 +# homeassistant.components.august +yalexs_ble==1.10.2 + # homeassistant.components.yeelight yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.1.0 +yolink-api==0.1.5 # homeassistant.components.youless youless-api==0.16 @@ -1811,10 +1840,10 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.87 +zha-quirks==0.0.88 # homeassistant.components.zha -zigpy-deconz==0.19.1 +zigpy-deconz==0.19.2 # homeassistant.components.zha zigpy-xbee==0.16.2 @@ -1823,13 +1852,13 @@ zigpy-xbee==0.16.2 zigpy-zigate==0.10.3 # homeassistant.components.zha -zigpy-znp==0.9.1 +zigpy-znp==0.9.2 # homeassistant.components.zha -zigpy==0.51.6 +zigpy==0.52.3 # homeassistant.components.zwave_js -zwave-js-server-python==0.43.0 +zwave-js-server-python==0.43.1 # homeassistant.components.zwave_me -zwave_me_ws==0.2.6 +zwave_me_ws==0.3.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ec6edeeea66..da49f1e7bc4 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,16 +1,17 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit +autoflake==2.0.0 bandit==1.7.4 black==22.10.0 -codespell==2.1.0 -flake8-comprehensions==3.10.0 +codespell==2.2.2 +flake8-comprehensions==3.10.1 flake8-docstrings==1.6.0 -flake8-noqa==1.2.8 -flake8==4.0.1 +flake8-noqa==1.3.0 +flake8==6.0.0 isort==5.10.1 -mccabe==0.6.1 -pycodestyle==2.8.0 +mccabe==0.7.0 +pycodestyle==2.10.0 pydocstyle==6.1.1 -pyflakes==2.4.0 -pyupgrade==3.1.0 +pyflakes==3.0.1 +pyupgrade==3.2.2 yamllint==1.28.0 diff --git a/script/bootstrap b/script/bootstrap index 5040a322b62..68f02961d27 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -8,4 +8,4 @@ cd "$(dirname "$0")/.." echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt -python3 -m pip install tox tox-pip-version colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt --use-deprecated=legacy-resolver +python3 -m pip install colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt --use-deprecated=legacy-resolver diff --git a/script/countries.py b/script/countries.py new file mode 100644 index 00000000000..0d776f0805d --- /dev/null +++ b/script/countries.py @@ -0,0 +1,27 @@ +"""Helper script to update country list. + +ISO does not publish a machine readable list free of charge, so the list is generated +with help of the pycountry package. +""" +from pathlib import Path + +import pycountry + +from .hassfest.serializer import format_python_namespace + +countries = {x.alpha_2 for x in pycountry.countries} + +generator_string = """script.countries + +The values are directly corresponding to the ISO 3166 standard. If you need changes +to the political situation in the world, please contact the ISO 3166 working group. +""" + +Path("homeassistant/generated/countries.py").write_text( + format_python_namespace( + { + "COUNTRIES": countries, + }, + generator=generator_string, + ) +) diff --git a/script/currencies.py b/script/currencies.py new file mode 100644 index 00000000000..753b3363626 --- /dev/null +++ b/script/currencies.py @@ -0,0 +1,44 @@ +"""Helper script to update currency list from the official source.""" +from pathlib import Path + +from bs4 import BeautifulSoup +import requests + +from .hassfest.serializer import format_python_namespace + +req = requests.get( + "https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml" +) +soup = BeautifulSoup(req.content, "xml") +active_currencies = { + x.Ccy.contents[0] + for x in soup.ISO_4217.CcyTbl.children + if x.name == "CcyNtry" + and x.Ccy + and x.CcyMnrUnts.contents[0] != "N.A." + and "IsFund" not in x.CcyNm.attrs + and x.Ccy.contents[0] != "UYW" +} + +req = requests.get( + "https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-three.xml" +) +soup = BeautifulSoup(req.content, "xml") +historic_currencies = { + x.Ccy.contents[0] + for x in soup.ISO_4217.HstrcCcyTbl.children + if x.name == "HstrcCcyNtry" + and x.Ccy + and "IsFund" not in x.CcyNm.attrs + and x.Ccy.contents[0] not in active_currencies +} + +Path("homeassistant/generated/currencies.py").write_text( + format_python_namespace( + { + "ACTIVE_CURRENCIES": active_currencies, + "HISTORIC_CURRENCIES": historic_currencies, + }, + generator="script.currencies", + ) +) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index bbc970f9178..355caa6cb62 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -"""Generate an updated requirements_all.txt.""" +"""Generate updated constraint and requirements files.""" +from __future__ import annotations + import difflib import importlib import os @@ -7,6 +9,7 @@ from pathlib import Path import pkgutil import re import sys +from typing import Any from homeassistant.util.yaml.loader import load_yaml from script.hassfest.model import Integration @@ -96,9 +99,9 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.6.1 -h11==0.12.0 -httpcore==0.15.0 +anyio==3.6.2 +h11==0.14.0 +httpcore==0.16.2 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation @@ -107,9 +110,6 @@ hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env numpy==1.23.2 -# pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead -pytest_asyncio==1000000000.0.0 - # Prevent dependency conflicts between sisyphus-control and aioambient # until upper bounds for sisyphus-control have been updated # https://github.com/jkeljo/sisyphus-control/issues/6 @@ -157,7 +157,7 @@ IGNORE_PRE_COMMIT_HOOK_ID = ( PACKAGE_REGEX = re.compile(r"^(?:--.+\s)?([-_\.\w\d]+).*==.+$") -def has_tests(module: str): +def has_tests(module: str) -> bool: """Test if a module has tests. Module format: homeassistant.components.hue @@ -169,11 +169,11 @@ def has_tests(module: str): return path.exists() -def explore_module(package, explore_children): +def explore_module(package: str, explore_children: bool) -> list[str]: """Explore the modules.""" module = importlib.import_module(package) - found = [] + found: list[str] = [] if not hasattr(module, "__path__"): return found @@ -187,14 +187,17 @@ def explore_module(package, explore_children): return found -def core_requirements(): +def core_requirements() -> list[str]: """Gather core requirements out of pyproject.toml.""" with open("pyproject.toml", "rb") as fp: data = tomllib.load(fp) - return data["project"]["dependencies"] + dependencies: list[str] = data["project"]["dependencies"] + return dependencies -def gather_recursive_requirements(domain, seen=None): +def gather_recursive_requirements( + domain: str, seen: set[str] | None = None +) -> set[str]: """Recursively gather requirements from a module.""" if seen is None: seen = set() @@ -221,18 +224,18 @@ def normalize_package_name(requirement: str) -> str: return package -def comment_requirement(req): +def comment_requirement(req: str) -> bool: """Comment out requirement. Some don't install on all systems.""" return any( normalize_package_name(req) == ign for ign in COMMENT_REQUIREMENTS_NORMALIZED ) -def gather_modules(): +def gather_modules() -> dict[str, list[str]] | None: """Collect the information.""" - reqs = {} + reqs: dict[str, list[str]] = {} - errors = [] + errors: list[str] = [] gather_requirements_from_manifests(errors, reqs) gather_requirements_from_modules(errors, reqs) @@ -248,16 +251,14 @@ def gather_modules(): return reqs -def gather_requirements_from_manifests(errors, reqs): +def gather_requirements_from_manifests( + errors: list[str], reqs: dict[str, list[str]] +) -> None: """Gather all of the requirements from manifests.""" integrations = Integration.load_dir(Path("homeassistant/components")) for domain in sorted(integrations): integration = integrations[domain] - if not integration.manifest: - errors.append(f"The manifest for integration {domain} is invalid.") - continue - if integration.disabled: continue @@ -266,7 +267,9 @@ def gather_requirements_from_manifests(errors, reqs): ) -def gather_requirements_from_modules(errors, reqs): +def gather_requirements_from_modules( + errors: list[str], reqs: dict[str, list[str]] +) -> None: """Collect the requirements from the modules directly.""" for package in sorted( explore_module("homeassistant.scripts", True) @@ -283,7 +286,12 @@ def gather_requirements_from_modules(errors, reqs): process_requirements(errors, module.REQUIREMENTS, package, reqs) -def process_requirements(errors, module_requirements, package, reqs): +def process_requirements( + errors: list[str], + module_requirements: list[str], + package: str, + reqs: dict[str, list[str]], +) -> None: """Process all of the requirements.""" for req in module_requirements: if "://" in req: @@ -293,7 +301,7 @@ def process_requirements(errors, module_requirements, package, reqs): reqs.setdefault(req, []).append(package) -def generate_requirements_list(reqs): +def generate_requirements_list(reqs: dict[str, list[str]]) -> str: """Generate a pip file based on requirements.""" output = [] for pkg, requirements in sorted(reqs.items(), key=lambda item: item[0]): @@ -307,7 +315,7 @@ def generate_requirements_list(reqs): return "".join(output) -def requirements_output(reqs): +def requirements_output() -> str: """Generate output for requirements.""" output = [ "-c homeassistant/package_constraints.txt\n", @@ -320,7 +328,7 @@ def requirements_output(reqs): return "".join(output) -def requirements_all_output(reqs): +def requirements_all_output(reqs: dict[str, list[str]]) -> str: """Generate output for requirements_all.""" output = [ "# Home Assistant Core, full dependency set\n", @@ -331,7 +339,7 @@ def requirements_all_output(reqs): return "".join(output) -def requirements_test_all_output(reqs): +def requirements_test_all_output(reqs: dict[str, list[str]]) -> str: """Generate output for test_requirements.""" output = [ "# Home Assistant tests, full dependency set\n", @@ -356,15 +364,18 @@ def requirements_test_all_output(reqs): return "".join(output) -def requirements_pre_commit_output(): +def requirements_pre_commit_output() -> str: """Generate output for pre-commit dependencies.""" source = ".pre-commit-config.yaml" - pre_commit_conf = load_yaml(source) - reqs = [] + pre_commit_conf: dict[str, list[dict[str, Any]]] + pre_commit_conf = load_yaml(source) # type: ignore[assignment] + reqs: list[str] = [] + hook: dict[str, Any] for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")): + rev: str = repo["rev"] for hook in repo["hooks"]: if hook["id"] not in IGNORE_PRE_COMMIT_HOOK_ID: - reqs.append(f"{hook['id']}=={repo['rev'].lstrip('v')}") + reqs.append(f"{hook['id']}=={rev.lstrip('v')}") reqs.extend(x for x in hook.get("additional_dependencies", ())) output = [ f"# Automatically generated " @@ -375,7 +386,7 @@ def requirements_pre_commit_output(): return "\n".join(output) + "\n" -def gather_constraints(): +def gather_constraints() -> str: """Construct output for constraint file.""" return ( "\n".join( @@ -392,7 +403,7 @@ def gather_constraints(): ) -def diff_file(filename, content): +def diff_file(filename: str, content: str) -> list[str]: """Diff a file.""" return list( difflib.context_diff( @@ -404,7 +415,7 @@ def diff_file(filename, content): ) -def main(validate): +def main(validate: bool) -> int: """Run the script.""" if not os.path.isfile("requirements_all.txt"): print("Run this from HA root dir") @@ -415,7 +426,7 @@ def main(validate): if data is None: return 1 - reqs_file = requirements_output(data) + reqs_file = requirements_output() reqs_all_file = requirements_all_output(data) reqs_test_all_file = requirements_test_all_output(data) reqs_pre_commit_file = requirements_pre_commit_output() diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 27252969118..87024619765 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -1,4 +1,6 @@ """Validate manifests.""" +from __future__ import annotations + import argparse import pathlib import sys @@ -55,7 +57,7 @@ ALL_PLUGIN_NAMES = [ ] -def valid_integration_path(integration_path): +def valid_integration_path(integration_path: pathlib.Path | str) -> pathlib.Path: """Test if it's a valid integration.""" path = pathlib.Path(integration_path) if not path.is_dir(): @@ -124,7 +126,7 @@ def get_config() -> Config: ) -def main(): +def main() -> int: """Validate manifests.""" try: config = get_config() @@ -218,7 +220,12 @@ def main(): return 1 -def print_integrations_status(config, integrations, *, show_fixable_errors=True): +def print_integrations_status( + config: Config, + integrations: list[Integration], + *, + show_fixable_errors: bool = True, +) -> None: """Print integration status.""" for integration in sorted(integrations, key=lambda itg: itg.domain): extra = f" - {integration.path}" if config.specific_integrations else "" diff --git a/script/hassfest/application_credentials.py b/script/hassfest/application_credentials.py index 2fb693bf429..1be644054c4 100644 --- a/script/hassfest/application_credentials.py +++ b/script/hassfest/application_credentials.py @@ -1,19 +1,8 @@ """Generate application_credentials data.""" from __future__ import annotations -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -APPLICATION_CREDENTIALS = {} -""".strip() +from .serializer import format_python_namespace def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: @@ -29,7 +18,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config) match_list.append(domain) - return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) + return format_python_namespace({"APPLICATION_CREDENTIALS": match_list}) def validate(integrations: dict[str, Integration], config: Config) -> None: @@ -52,7 +41,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: ) -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate application_credentials data.""" application_credentials_path = ( config.root / "homeassistant/generated/application_credentials.py" diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py index 0b57b1084e8..295bcac1d1e 100644 --- a/script/hassfest/bluetooth.py +++ b/script/hassfest/bluetooth.py @@ -1,33 +1,16 @@ """Generate bluetooth file.""" from __future__ import annotations -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" -from __future__ import annotations - -BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = {} -""".strip() +from .serializer import format_python_namespace -def generate_and_validate(integrations: list[dict[str, str]]): +def generate_and_validate(integrations: dict[str, Integration]) -> str: """Validate and generate bluetooth data.""" match_list = [] for domain in sorted(integrations): - integration = integrations[domain] - - if not integration.manifest or not integration.config_flow: - continue - - match_types = integration.manifest.get("bluetooth", []) + match_types = integrations[domain].manifest.get("bluetooth", []) if not match_types: continue @@ -35,10 +18,13 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) + return format_python_namespace( + {"BLUETOOTH": match_list}, + annotations={"BLUETOOTH": "list[dict[str, bool | str | int | list[int]]]"}, + ) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate bluetooth file.""" bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" config.cache["bluetooth"] = content = generate_and_validate(integrations) @@ -57,7 +43,7 @@ def validate(integrations: dict[str, Integration], config: Config): return -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate bluetooth file.""" bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" with open(str(bluetooth_path), "w") as fp: diff --git a/script/hassfest/brand.py b/script/hassfest/brand.py index c35f50599ff..1083cc911bd 100644 --- a/script/hassfest/brand.py +++ b/script/hassfest/brand.py @@ -55,7 +55,7 @@ def _validate_brand( ): config.add_error( "brand", - f"{brand.path.name}: Brand '{brand.brand['domain']}' " + f"{brand.path.name}: Brand '{brand.domain}' " f"is an integration but is missing in the brand's 'integrations' list'", ) diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 0cc58012162..f95a7b3b542 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -42,17 +42,14 @@ REMOVE_CODEOWNERS = """ """ -def generate_and_validate(integrations: dict[str, Integration], config: Config): +def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: """Generate CODEOWNERS.""" parts = [BASE] for domain in sorted(integrations): integration = integrations[domain] - if ( - not integration.manifest - or integration.manifest.get("integration_type") == "virtual" - ): + if integration.integration_type == "virtual": continue codeowners = integration.manifest["codeowners"] @@ -77,7 +74,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): return "\n".join(parts) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate CODEOWNERS.""" codeowners_path = config.root / "CODEOWNERS" config.cache["codeowners"] = content = generate_and_validate(integrations, config) @@ -95,7 +92,7 @@ def validate(integrations: dict[str, Integration], config: Config): return -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate CODEOWNERS.""" codeowners_path = config.root / "CODEOWNERS" with open(str(codeowners_path), "w") as fp: diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 84347697147..5ede5daaa35 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -3,26 +3,16 @@ from __future__ import annotations import json import pathlib - -import black +from typing import Any from .brand import validate as validate_brands from .model import Brand, Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -FLOWS = {} -""".strip() +from .serializer import format_python_namespace UNIQUE_ID_IGNORE = {"huawei_lte", "mqtt", "adguard"} -def _validate_integration(config: Config, integration: Integration): +def _validate_integration(config: Config, integration: Integration) -> None: """Validate config flow of an integration.""" config_flow_file = integration.path / "config_flow.py" @@ -71,17 +61,16 @@ def _validate_integration(config: Config, integration: Integration): ) -def _generate_and_validate(integrations: dict[str, Integration], config: Config): +def _generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: """Validate and generate config flow data.""" - domains = { + domains: dict[str, list[str]] = { "integration": [], "helper": [], } for domain in sorted(integrations): integration = integrations[domain] - - if not integration.manifest or not integration.config_flow: + if not integration.config_flow: continue _validate_integration(config, integration) @@ -91,13 +80,13 @@ def _generate_and_validate(integrations: dict[str, Integration], config: Config) else: domains["integration"].append(domain) - return black.format_str(BASE.format(to_string(domains)), mode=black.Mode()) + return format_python_namespace({"FLOWS": domains}) def _populate_brand_integrations( - integration_data: dict, + integration_data: dict[str, Any], integrations: dict[str, Integration], - brand_metadata: dict, + brand_metadata: dict[str, Any], sub_integrations: list[str], ) -> None: """Add referenced integrations to a brand's metadata.""" @@ -110,7 +99,7 @@ def _populate_brand_integrations( "system", ): continue - metadata = { + metadata: dict[str, Any] = { "integration_type": integration.integration_type, } # Always set the config_flow key to avoid breaking the frontend @@ -130,11 +119,13 @@ def _populate_brand_integrations( def _generate_integrations( - brands: dict[str, Brand], integrations: dict[str, Integration], config: Config -): + brands: dict[str, Brand], + integrations: dict[str, Integration], + config: Config, +) -> str: """Generate integrations data.""" - result = { + result: dict[str, Any] = { "integration": {}, "helper": {}, "translated_name": set(), @@ -158,14 +149,14 @@ def _generate_integrations( primary_domains = { domain for domain, integration in integrations.items() - if integration.manifest and domain not in brand_integration_domains + if domain not in brand_integration_domains } # Add all brands to the set primary_domains |= set(brands) # Generate the config flow index for domain in sorted(primary_domains): - metadata = {} + metadata: dict[str, Any] = {} if brand := brands.get(domain): metadata["name"] = brand.name @@ -210,7 +201,7 @@ def _generate_integrations( ) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate config flow file.""" config_flow_path = config.root / "homeassistant/generated/config_flows.py" integrations_path = config.root / "homeassistant/generated/integrations.json" @@ -244,7 +235,7 @@ def validate(integrations: dict[str, Integration], config: Config): ) -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate config flow file.""" config_flow_path = config.root / "homeassistant/generated/config_flows.py" integrations_path = config.root / "homeassistant/generated/integrations.json" diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 7c259adbfa3..71d2e3ce57c 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -30,7 +30,7 @@ ALLOWED_IGNORE_VIOLATIONS = { } -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate coverage.""" coverage_path = config.root / ".coveragerc" diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index b5d82f7b348..9993d5c52a9 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -7,19 +7,19 @@ from pathlib import Path from homeassistant.const import Platform from homeassistant.requirements import DISCOVERY_INTEGRATIONS -from .model import Integration +from .model import Config, Integration class ImportCollector(ast.NodeVisitor): """Collect all integrations referenced.""" - def __init__(self, integration: Integration): + def __init__(self, integration: Integration) -> None: """Initialize the import collector.""" self.integration = integration self.referenced: dict[Path, set[str]] = {} # Current file or dir we're inspecting - self._cur_fil_dir = None + self._cur_fil_dir: Path | None = None def collect(self) -> None: """Collect imports from a source file.""" @@ -32,11 +32,12 @@ class ImportCollector(ast.NodeVisitor): self.visit(ast.parse(fil.read_text())) self._cur_fil_dir = None - def _add_reference(self, reference_domain: str): + def _add_reference(self, reference_domain: str) -> None: """Add a reference.""" + assert self._cur_fil_dir self.referenced[self._cur_fil_dir].add(reference_domain) - def visit_ImportFrom(self, node): + def visit_ImportFrom(self, node: ast.ImportFrom) -> None: """Visit ImportFrom node.""" if node.module is None: return @@ -59,14 +60,14 @@ class ImportCollector(ast.NodeVisitor): for name_node in node.names: self._add_reference(name_node.name) - def visit_Import(self, node): + def visit_Import(self, node: ast.Import) -> None: """Visit Import node.""" # import homeassistant.components.hue as hue for name_node in node.names: if name_node.name.startswith("homeassistant.components."): self._add_reference(name_node.name.split(".")[2]) - def visit_Attribute(self, node): + def visit_Attribute(self, node: ast.Attribute) -> None: """Visit Attribute node.""" # hass.components.hue.async_create() # Name(id=hass) @@ -156,15 +157,16 @@ IGNORE_VIOLATIONS = { def calc_allowed_references(integration: Integration) -> set[str]: """Return a set of allowed references.""" + manifest = integration.manifest allowed_references = ( ALLOWED_USED_COMPONENTS - | set(integration.manifest.get("dependencies", [])) - | set(integration.manifest.get("after_dependencies", [])) + | set(manifest.get("dependencies", [])) + | set(manifest.get("after_dependencies", [])) ) # Discovery requirements are ok if referenced in manifest for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): - if any(check in integration.manifest for check in to_check): + if any(check in manifest for check in to_check): allowed_references.add(check_domain) return allowed_references @@ -174,8 +176,8 @@ def find_non_referenced_integrations( integrations: dict[str, Integration], integration: Integration, references: dict[Path, set[str]], -): - """Find intergrations that are not allowed to be referenced.""" +) -> set[str]: + """Find integrations that are not allowed to be referenced.""" allowed_references = calc_allowed_references(integration) referenced = set() for path, refs in references.items(): @@ -219,8 +221,9 @@ def find_non_referenced_integrations( def validate_dependencies( - integrations: dict[str, Integration], integration: Integration -): + integrations: dict[str, Integration], + integration: Integration, +) -> None: """Validate all dependencies.""" # Some integrations are allowed to have violations. if integration.domain in IGNORE_VIOLATIONS: @@ -242,13 +245,10 @@ def validate_dependencies( ) -def validate(integrations: dict[str, Integration], config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle dependencies for integrations.""" # check for non-existing dependencies for integration in integrations.values(): - if not integration.manifest: - continue - validate_dependencies(integrations, integration) if config.specific_integrations: diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py index c246acec5f0..882e39f300e 100644 --- a/script/hassfest/dhcp.py +++ b/script/hassfest/dhcp.py @@ -1,32 +1,16 @@ """Generate dhcp file.""" from __future__ import annotations -import black - from .model import Config, Integration - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" -from __future__ import annotations - -DHCP: list[dict[str, str | bool]] = {} -""".strip() +from .serializer import format_python_namespace -def generate_and_validate(integrations: list[dict[str, str]]): +def generate_and_validate(integrations: dict[str, Integration]) -> str: """Validate and generate dhcp data.""" match_list = [] for domain in sorted(integrations): - integration = integrations[domain] - - if not integration.manifest or not integration.config_flow: - continue - - match_types = integration.manifest.get("dhcp", []) + match_types = integrations[domain].manifest.get("dhcp", []) if not match_types: continue @@ -34,10 +18,13 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - return black.format_str(BASE.format(str(match_list)), mode=black.Mode()) + return format_python_namespace( + {"DHCP": match_list}, + annotations={"DHCP": "list[dict[str, str | bool]]"}, + ) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate dhcp file.""" dhcp_path = config.root / "homeassistant/generated/dhcp.py" config.cache["dhcp"] = content = generate_and_validate(integrations) @@ -56,7 +43,7 @@ def validate(integrations: dict[str, Integration], config: Config): return -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate dhcp file.""" dhcp_path = config.root / "homeassistant/generated/dhcp.py" with open(str(dhcp_path), "w") as fp: diff --git a/script/hassfest/json.py b/script/hassfest/json.py index 49ebb05bbea..5e3d05f78dc 100644 --- a/script/hassfest/json.py +++ b/script/hassfest/json.py @@ -3,10 +3,10 @@ from __future__ import annotations import json -from .model import Integration +from .model import Config, Integration -def validate_json_files(integration: Integration): +def validate_json_files(integration: Integration) -> None: """Validate JSON files for integration.""" for json_file in integration.path.glob("**/*.json"): if not json_file.is_file(): @@ -18,16 +18,11 @@ def validate_json_files(integration: Integration): relative_path = json_file.relative_to(integration.path) integration.add_error("json", f"Invalid JSON file {relative_path}") - return - -def validate(integrations: dict[str, Integration], config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle JSON files inside integrations.""" if not config.specific_integrations: return for integration in integrations.values(): - if not integration.manifest: - continue - validate_json_files(integration) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 874fb069818..130d3288ab6 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -59,6 +59,7 @@ NO_IOT_CLASS = [ "history", "homeassistant", "homeassistant_alerts", + "homeassistant_hardware", "homeassistant_sky_connect", "homeassistant_yellow", "image", @@ -118,7 +119,7 @@ def documentation_url(value: str) -> str: return value -def verify_lowercase(value: str): +def verify_lowercase(value: str) -> str: """Verify a value is lowercase.""" if value.lower() != value: raise vol.Invalid("Value needs to be lowercase") @@ -126,7 +127,7 @@ def verify_lowercase(value: str): return value -def verify_uppercase(value: str): +def verify_uppercase(value: str) -> str: """Verify a value is uppercase.""" if value.upper() != value: raise vol.Invalid("Value needs to be uppercase") @@ -134,7 +135,7 @@ def verify_uppercase(value: str): return value -def verify_version(value: str): +def verify_version(value: str) -> str: """Verify the version.""" try: AwesomeVersion( @@ -152,7 +153,7 @@ def verify_version(value: str): return value -def verify_wildcard(value: str): +def verify_wildcard(value: str) -> str: """Verify the matcher contains a wildcard.""" if "*" not in value: raise vol.Invalid(f"'{value}' needs to contain a wildcard matcher") @@ -285,7 +286,7 @@ CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend( ) -def validate_version(integration: Integration): +def validate_version(integration: Integration) -> None: """ Validate the version of the integration. @@ -298,9 +299,6 @@ def validate_version(integration: Integration): def validate_manifest(integration: Integration, core_components_dir: Path) -> None: """Validate manifest.""" - if not integration.manifest: - return - try: if integration.core: manifest_schema(integration.manifest) diff --git a/script/hassfest/manifest_helper.py b/script/hassfest/manifest_helper.py deleted file mode 100644 index 0c2a1456ec6..00000000000 --- a/script/hassfest/manifest_helper.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Helpers to deal with manifests.""" -import json -import pathlib - -component_dir = pathlib.Path("homeassistant/components") - - -def iter_manifests(): - """Iterate over all available manifests.""" - manifests = [ - json.loads(fil.read_text()) for fil in component_dir.glob("*/manifest.json") - ] - return sorted(manifests, key=lambda man: man["domain"]) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 65d3b1144e8..ee0a3ab32d9 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -1,7 +1,6 @@ """Models for manifest validator.""" from __future__ import annotations -import importlib import json import pathlib from typing import Any @@ -26,7 +25,7 @@ class Error: class Config: """Config for the run.""" - specific_integrations: pathlib.Path | None = attr.ib() + specific_integrations: list[pathlib.Path] | None = attr.ib() root: pathlib.Path = attr.ib() action: str = attr.ib() requirements: bool = attr.ib() @@ -44,10 +43,10 @@ class Brand: """Represent a brand in our validator.""" @classmethod - def load_dir(cls, path: pathlib.Path, config: Config): + def load_dir(cls, path: pathlib.Path, config: Config) -> dict[str, Brand]: """Load all brands in a directory.""" assert path.is_dir() - brands = {} + brands: dict[str, Brand] = {} for fil in path.iterdir(): brand = cls(fil) brand.load_brand(config) @@ -56,7 +55,13 @@ class Brand: return brands path: pathlib.Path = attr.ib() - brand: dict[str, Any] | None = attr.ib(default=None) + _brand: dict[str, Any] | None = attr.ib(default=None) + + @property + def brand(self) -> dict[str, Any]: + """Guarded access to brand.""" + assert self._brand is not None, "brand has not been loaded" + return self._brand @property def domain(self) -> str: @@ -71,7 +76,7 @@ class Brand: @property def integrations(self) -> list[str]: """Return the sub integrations of this brand.""" - return self.brand.get("integrations") + return self.brand.get("integrations", []) @property def iot_standards(self) -> list[str]: @@ -85,14 +90,14 @@ class Brand: return try: - brand = json.loads(self.path.read_text()) + brand: dict[str, Any] = json.loads(self.path.read_text()) except ValueError as err: config.add_error( "model", f"Brand file {self.path.name} contains invalid JSON: {err}" ) return - self.brand = brand + self._brand = brand @attr.s @@ -100,10 +105,10 @@ class Integration: """Represent an integration in our validator.""" @classmethod - def load_dir(cls, path: pathlib.Path): + def load_dir(cls, path: pathlib.Path) -> dict[str, Integration]: """Load all integrations in a directory.""" assert path.is_dir() - integrations = {} + integrations: dict[str, Integration] = {} for fil in path.iterdir(): if fil.is_file() or fil.name == "__pycache__": continue @@ -125,11 +130,17 @@ class Integration: return integrations path: pathlib.Path = attr.ib() - manifest: dict[str, Any] | None = attr.ib(default=None) + _manifest: dict[str, Any] | None = attr.ib(default=None) errors: list[Error] = attr.ib(factory=list) warnings: list[Error] = attr.ib(factory=list) translated_name: bool = attr.ib(default=False) + @property + def manifest(self) -> dict[str, Any]: + """Guarded access to manifest.""" + assert self._manifest is not None, "manifest has not been loaded" + return self._manifest + @property def domain(self) -> str: """Integration domain.""" @@ -148,10 +159,11 @@ class Integration: @property def name(self) -> str: """Return name of the integration.""" - return self.manifest["name"] + name: str = self.manifest["name"] + return name @property - def quality_scale(self) -> str: + def quality_scale(self) -> str | None: """Return quality scale of the integration.""" return self.manifest.get("quality_scale") @@ -206,16 +218,9 @@ class Integration: return try: - manifest = json.loads(manifest_path.read_text()) + manifest: dict[str, Any] = json.loads(manifest_path.read_text()) except ValueError as err: self.add_error("model", f"Manifest contains invalid JSON: {err}") return - self.manifest = manifest - - def import_pkg(self, platform=None): - """Import the Python file.""" - pkg = f"homeassistant.components.{self.domain}" - if platform is not None: - pkg += f".{platform}" - return importlib.import_module(pkg) + self._manifest = manifest diff --git a/script/hassfest/mqtt.py b/script/hassfest/mqtt.py index ab5f159026e..2619e22911a 100644 --- a/script/hassfest/mqtt.py +++ b/script/hassfest/mqtt.py @@ -3,33 +3,17 @@ from __future__ import annotations from collections import defaultdict -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -MQTT = {} -""".strip() +from .serializer import format_python_namespace -def generate_and_validate(integrations: dict[str, Integration]): +def generate_and_validate(integrations: dict[str, Integration]) -> str: """Validate and generate MQTT data.""" data = defaultdict(list) for domain in sorted(integrations): - integration = integrations[domain] - - if not integration.manifest or not integration.config_flow: - continue - - mqtt = integration.manifest.get("mqtt") + mqtt = integrations[domain].manifest.get("mqtt") if not mqtt: continue @@ -37,10 +21,10 @@ def generate_and_validate(integrations: dict[str, Integration]): for topic in mqtt: data[domain].append(topic) - return black.format_str(BASE.format(to_string(data)), mode=black.Mode()) + return format_python_namespace({"MQTT": data}) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate MQTT file.""" mqtt_path = config.root / "homeassistant/generated/mqtt.py" config.cache["mqtt"] = content = generate_and_validate(integrations) @@ -55,10 +39,9 @@ def validate(integrations: dict[str, Integration], config: Config): "File mqtt.py is not up to date. Run python3 -m script.hassfest", fixable=True, ) - return -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate MQTT file.""" mqtt_path = config.root / "homeassistant/generated/mqtt.py" with open(str(mqtt_path), "w") as fp: diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 0c598df9cd1..e6886f2f79f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -1,6 +1,7 @@ """Generate mypy config.""" from __future__ import annotations +from collections.abc import Iterable import configparser import io import os @@ -46,7 +47,8 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "warn_redundant_casts": "true", "warn_unused_configs": "true", "warn_unused_ignores": "true", - "enable_error_code": "ignore-without-code", + "enable_error_code": ", ".join(["ignore-without-code"]), + "disable_error_code": ", ".join(["annotation-unchecked"]), # Strict_concatenate breaks passthrough ParamSpec typing "strict_concatenate": "false", } @@ -89,18 +91,48 @@ def _strict_module_in_ignore_list( return None -def generate_and_validate(config: Config) -> str: +def _sort_within_sections(line_iter: Iterable[str]) -> Iterable[str]: + """ + Sort lines within sections. + + Sections are defined as anything not delimited by a blank line + or an octothorpe-prefixed comment line. + """ + section: list[str] = [] + for line in line_iter: + if line.startswith("#") or not line.strip(): + yield from sorted(section) + section.clear() + yield line + continue + section.append(line) + yield from sorted(section) + + +def _get_strict_typing_path(config: Config) -> Path: + return config.root / ".strict-typing" + + +def _get_mypy_ini_path(config: Config) -> Path: + return config.root / "mypy.ini" + + +def _generate_and_validate_strict_typing(config: Config) -> str: + """Validate and generate strict_typing.""" + lines = [ + line.strip() + for line in _get_strict_typing_path(config).read_text().splitlines() + ] + return "\n".join(_sort_within_sections(lines)) + "\n" + + +def _generate_and_validate_mypy_config(config: Config) -> str: """Validate and generate mypy config.""" - config_path = config.root / ".strict-typing" - - with config_path.open() as fp: - lines = fp.readlines() - # Filter empty and commented lines. parsed_modules: list[str] = [ line.strip() - for line in lines + for line in config.cache["strict_typing"].splitlines() if line.strip() != "" and not line.startswith("#") ] @@ -209,28 +241,36 @@ def generate_and_validate(config: Config) -> str: with io.StringIO() as fp: mypy_config.write(fp) fp.seek(0) - return HEADER + fp.read().strip() + return f"{HEADER}{fp.read().strip()}\n" def validate(integrations: dict[str, Integration], config: Config) -> None: - """Validate mypy config.""" - config_path = config.root / "mypy.ini" - config.cache["mypy_config"] = content = generate_and_validate(config) + """Validate strict_typing and mypy config.""" + strict_typing_content = _generate_and_validate_strict_typing(config) + config.cache["strict_typing"] = strict_typing_content + + mypy_content = _generate_and_validate_mypy_config(config) + config.cache["mypy_config"] = mypy_content if any(err.plugin == "mypy_config" for err in config.errors): return - with open(str(config_path)) as fp: - if fp.read().strip() != content: - config.add_error( - "mypy_config", - "File mypy.ini is not up to date. Run python3 -m script.hassfest", - fixable=True, - ) + if _get_strict_typing_path(config).read_text() != strict_typing_content: + config.add_error( + "mypy_config", + "File .strict_typing is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + + if _get_mypy_ini_path(config).read_text() != mypy_content: + config.add_error( + "mypy_config", + "File mypy.ini is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) def generate(integrations: dict[str, Integration], config: Config) -> None: - """Generate mypy config.""" - config_path = config.root / "mypy.ini" - with open(str(config_path), "w") as fp: - fp.write(f"{config.cache['mypy_config']}\n") + """Generate strict_typing and mypy config.""" + _get_mypy_ini_path(config).write_text(config.cache["mypy_config"]) + _get_strict_typing_path(config).write_text(config.cache["strict_typing"]) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 510a70f30ce..27dd0654dc6 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -2,11 +2,13 @@ from __future__ import annotations from collections import deque +from functools import cache import json import os import re import subprocess import sys +from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from stdlib_list import stdlib_list @@ -31,12 +33,12 @@ SUPPORTED_PYTHON_TUPLES = [ ] if REQUIRED_PYTHON_VER[0] == REQUIRED_NEXT_PYTHON_VER[0]: for minor in range(REQUIRED_PYTHON_VER[1] + 1, REQUIRED_NEXT_PYTHON_VER[1] + 1): - SUPPORTED_PYTHON_TUPLES.append((REQUIRED_PYTHON_VER[0], minor)) + if minor < 10: # stdlib list does not support 3.10+ + SUPPORTED_PYTHON_TUPLES.append((REQUIRED_PYTHON_VER[0], minor)) SUPPORTED_PYTHON_VERSIONS = [ ".".join(map(str, version_tuple)) for version_tuple in SUPPORTED_PYTHON_TUPLES ] STD_LIBS = {version: set(stdlib_list(version)) for version in SUPPORTED_PYTHON_VERSIONS} -PIPDEPTREE_CACHE = None IGNORE_VIOLATIONS = { # Still has standard library requirements. @@ -52,7 +54,7 @@ IGNORE_VIOLATIONS = { } -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle requirements for integrations.""" # Check if we are doing format-only validation. if not config.requirements: @@ -60,16 +62,11 @@ def validate(integrations: dict[str, Integration], config: Config): validate_requirements_format(integration) return - ensure_cache() - # check for incompatible requirements - disable_tqdm = config.specific_integrations or os.environ.get("CI", False) + disable_tqdm = bool(config.specific_integrations or os.environ.get("CI")) for integration in tqdm(integrations.values(), disable=disable_tqdm): - if not integration.manifest: - continue - validate_requirements(integration) @@ -88,7 +85,13 @@ def validate_requirements_format(integration: Integration) -> bool: ) continue - pkg, sep, version = PACKAGE_REGEX.match(req).groups() + if not (match := PACKAGE_REGEX.match(req)): + integration.add_error( + "requirements", + f'Requirement "{req}" does not match package regex pattern', + ) + continue + pkg, sep, version = match.groups() if integration.core and sep != "==": integration.add_error( @@ -116,7 +119,7 @@ def validate_requirements_format(integration: Integration) -> bool: return len(integration.errors) == start_errors -def validate_requirements(integration: Integration): +def validate_requirements(integration: Integration) -> None: """Validate requirements.""" if not validate_requirements_format(integration): return @@ -167,8 +170,9 @@ def validate_requirements(integration: Integration): ) -def ensure_cache(): - """Ensure we have a cache of pipdeptree. +@cache +def get_pipdeptree() -> dict[str, dict[str, Any]]: + """Get pipdeptree output. Cached on first invocation. { "flake8-docstring": { @@ -179,12 +183,7 @@ def ensure_cache(): } } """ - global PIPDEPTREE_CACHE - - if PIPDEPTREE_CACHE is not None: - return - - cache = {} + deptree = {} for item in json.loads( subprocess.run( @@ -194,17 +193,16 @@ def ensure_cache(): text=True, ).stdout ): - cache[item["package"]["key"]] = { + deptree[item["package"]["key"]] = { **item["package"], "dependencies": {dep["key"] for dep in item["dependencies"]}, } - - PIPDEPTREE_CACHE = cache + return deptree def get_requirements(integration: Integration, packages: set[str]) -> set[str]: """Return all (recursively) requirements for an integration.""" - ensure_cache() + deptree = get_pipdeptree() all_requirements = set() @@ -218,7 +216,7 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: all_requirements.add(package) - item = PIPDEPTREE_CACHE.get(package) + item = deptree.get(package) if item is None: # Only warn if direct dependencies could not be resolved @@ -238,9 +236,7 @@ def install_requirements(integration: Integration, requirements: set[str]) -> bo Return True if successful. """ - global PIPDEPTREE_CACHE - - ensure_cache() + deptree = get_pipdeptree() for req in requirements: match = PIP_REGEX.search(req) @@ -261,8 +257,8 @@ def install_requirements(integration: Integration, requirements: set[str]) -> bo if normalized and "==" in requirement_arg: ver = requirement_arg.split("==")[-1] - item = PIPDEPTREE_CACHE.get(normalized) - is_installed = item and item["installed_version"] == ver + item = deptree.get(normalized) + is_installed = bool(item and item["installed_version"] == ver) if not is_installed: try: @@ -287,7 +283,7 @@ def install_requirements(integration: Integration, requirements: set[str]) -> bo else: # Clear the pipdeptree cache if something got installed if "Successfully installed" in result.stdout: - PIPDEPTREE_CACHE = None + get_pipdeptree.cache_clear() if integration.errors: return False diff --git a/script/hassfest/serializer.py b/script/hassfest/serializer.py index 8a6f410c345..41f6a554aff 100644 --- a/script/hassfest/serializer.py +++ b/script/hassfest/serializer.py @@ -1,37 +1,100 @@ """Hassfest utils.""" from __future__ import annotations +from collections.abc import Collection, Iterable, Mapping from typing import Any +import black +from black.mode import Mode -def _dict_to_str(data: dict) -> str: - """Return a string representation of a dict.""" - items = [f"'{key}':{to_string(value)}" for key, value in data.items()] - result = "{" - for item in items: - result += str(item) - result += "," - result += "}" - return result +DEFAULT_GENERATOR = "script.hassfest" -def _list_to_str(data: dict) -> str: - """Return a string representation of a list.""" - items = [to_string(value) for value in data] - result = "[" - for item in items: - result += str(item) - result += "," - result += "]" - return result +def _wrap_items( + items: Iterable[str], + opener: str, + closer: str, + sort: bool = False, +) -> str: + """Wrap pre-formatted Python reprs in braces, optionally sorting them.""" + # The trailing comma is imperative so Black doesn't format some items + # on one line and some on multiple. + if sort: + items = sorted(items) + + joined_items = ", ".join(items) + return f"{opener}{joined_items}{',' if joined_items else ''}{closer}" + + +def _mapping_to_str(data: Mapping[Any, Any]) -> str: + """Return a string representation of a mapping.""" + return _wrap_items( + (f"{to_string(key)}:{to_string(value)}" for key, value in data.items()), + opener="{", + closer="}", + sort=True, + ) + + +def _collection_to_str( + data: Collection[Any], + opener: str = "[", + closer: str = "]", + sort: bool = False, +) -> str: + """Return a string representation of a collection.""" + items = (to_string(value) for value in data) + return _wrap_items(items, opener, closer, sort=sort) def to_string(data: Any) -> str: """Return a string representation of the input.""" if isinstance(data, dict): - return _dict_to_str(data) + return _mapping_to_str(data) if isinstance(data, list): - return _list_to_str(data) - if isinstance(data, str): - return "'" + data + "'" - return data + return _collection_to_str(data) + if isinstance(data, set): + return _collection_to_str(data, "{", "}", sort=True) + return repr(data) + + +def format_python( + content: str, + *, + generator: str = DEFAULT_GENERATOR, +) -> str: + """Format Python code with Black. Optionally prepend a generator comment.""" + if generator: + content = f"""\"\"\"This file is automatically generated. + +To update, run python3 -m {generator} +\"\"\" + +{content} +""" + return black.format_str(content.strip(), mode=Mode()) + + +def format_python_namespace( + content: dict[str, Any], + *, + annotations: dict[str, str] | None = None, + generator: str = DEFAULT_GENERATOR, +) -> str: + """Generate a nicely formatted "namespace" file. + + The keys of the `content` dict will be used as variable names. + """ + + def _get_annotation(key: str) -> str: + annotation = (annotations or {}).get(key) + return f": {annotation}" if annotation else "" + + code = "\n\n".join( + f"{key}{_get_annotation(key)}" f" = {to_string(value)}" + for key, value in sorted(content.items()) + ) + if annotations: + # If we had any annotations, add the __future__ import. + code = f"from __future__ import annotations\n{code}" + return format_python(code, generator=generator) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index a5d10f8dda5..d9351b80370 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -3,6 +3,7 @@ from __future__ import annotations import pathlib import re +from typing import Any import voluptuous as vol from voluptuous.humanize import humanize_error @@ -12,10 +13,10 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, selector from homeassistant.util.yaml import load_yaml -from .model import Integration +from .model import Config, Integration -def exists(value): +def exists(value: Any) -> Any: """Check if value exists.""" if value is None: raise vol.Invalid("Value cannot be None") @@ -63,7 +64,7 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool return False -def validate_services(integration: Integration): +def validate_services(integration: Integration) -> None: """Validate services.""" try: data = load_yaml(str(integration.path / "services.yaml")) @@ -92,11 +93,8 @@ def validate_services(integration: Integration): ) -def validate(integrations: dict[str, Integration], config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle dependencies for integrations.""" # check services.yaml is cool for integration in integrations.values(): - if not integration.manifest: - continue - validate_services(integration) diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 599746e9874..de548ee2992 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -3,49 +3,28 @@ from __future__ import annotations from collections import defaultdict -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -SSDP = {} -""".strip() +from .serializer import format_python_namespace -def sort_dict(value): - """Sort a dictionary.""" - return {key: value[key] for key in sorted(value)} - - -def generate_and_validate(integrations: dict[str, Integration]): +def generate_and_validate(integrations: dict[str, Integration]) -> str: """Validate and generate ssdp data.""" data = defaultdict(list) for domain in sorted(integrations): - integration = integrations[domain] - - if not integration.manifest or not integration.config_flow: - continue - - ssdp = integration.manifest.get("ssdp") + ssdp = integrations[domain].manifest.get("ssdp") if not ssdp: continue for matcher in ssdp: - data[domain].append(sort_dict(matcher)) + data[domain].append(matcher) - return black.format_str(BASE.format(to_string(data)), mode=black.Mode()) + return format_python_namespace({"SSDP": data}) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate ssdp file.""" ssdp_path = config.root / "homeassistant/generated/ssdp.py" config.cache["ssdp"] = content = generate_and_validate(integrations) @@ -60,10 +39,9 @@ def validate(integrations: dict[str, Integration], config: Config): "File ssdp.py is not up to date. Run python3 -m script.hassfest", fixable=True, ) - return -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate ssdp file.""" ssdp_path = config.root / "homeassistant/generated/ssdp.py" with open(str(ssdp_path), "w") as fp: diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index ab2961e0506..824ebc6b825 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -5,6 +5,7 @@ from functools import partial from itertools import chain import json import re +from typing import Any import voluptuous as vol from voluptuous.humanize import humanize_error @@ -51,7 +52,7 @@ MOVED_TRANSLATIONS_DIRECTORY_MSG = ( ) -def allow_name_translation(integration: Integration): +def allow_name_translation(integration: Integration) -> bool: """Validate that the translation name is not the same as the integration name.""" # Only enforce for core because custom integrations can't be # added to allow list. @@ -74,7 +75,11 @@ def check_translations_directory_name(integration: Integration) -> None: integration.add_error("translations", MOVED_TRANSLATIONS_DIRECTORY_MSG) -def find_references(strings, prefix, found): +def find_references( + strings: dict[str, Any], + prefix: str, + found: list[dict[str, str]], +) -> None: """Find references.""" for key, value in strings.items(): if isinstance(value, dict): @@ -87,7 +92,11 @@ def find_references(strings, prefix, found): found.append({"source": f"{prefix}::{key}", "ref": match.groups()[0]}) -def removed_title_validator(config, integration, value): +def removed_title_validator( + config: Config, + integration: Integration, + value: Any, +) -> Any: """Mark removed title.""" if not config.specific_integrations: raise vol.Invalid(REMOVED_TITLE_MSG) @@ -97,7 +106,7 @@ def removed_title_validator(config, integration, value): return value -def lowercase_validator(value): +def lowercase_validator(value: str) -> str: """Validate value is lowercase.""" if value.lower() != value: raise vol.Invalid("Needs to be lowercase") @@ -112,7 +121,7 @@ def gen_data_entry_schema( flow_title: int, require_step_title: bool, mandatory_description: str | None = None, -): +) -> vol.All: """Generate a data entry schema.""" step_title_class = vol.Required if require_step_title else vol.Optional schema = { @@ -138,7 +147,7 @@ def gen_data_entry_schema( removed_title_validator, config, integration ) - def data_description_validator(value): + def data_description_validator(value: dict[str, Any]) -> dict[str, Any]: """Validate data description.""" for step_info in value["step"].values(): if "data_description" not in step_info: @@ -154,7 +163,7 @@ def gen_data_entry_schema( if mandatory_description is not None: - def validate_description_set(value): + def validate_description_set(value: dict[str, Any]) -> dict[str, Any]: """Validate description is set.""" steps = value["step"] if mandatory_description not in steps: @@ -169,7 +178,7 @@ def gen_data_entry_schema( if not allow_name_translation(integration): - def name_validator(value): + def name_validator(value: dict[str, Any]) -> dict[str, Any]: """Validate name.""" for step_id, info in value["step"].items(): if info.get("title") == integration.name: @@ -250,7 +259,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: ) -def gen_auth_schema(config: Config, integration: Integration): +def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: """Generate auth schema.""" return vol.Schema( { @@ -266,7 +275,23 @@ def gen_auth_schema(config: Config, integration: Integration): ) -def gen_platform_strings_schema(config: Config, integration: Integration): +def gen_ha_hardware_schema(config: Config, integration: Integration): + """Generate auth schema.""" + return vol.Schema( + { + str: { + vol.Optional("options"): gen_data_entry_schema( + config=config, + integration=integration, + flow_title=UNDEFINED, + require_step_title=False, + ) + } + } + ) + + +def gen_platform_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate platform strings schema like strings.sensor.json. Example of valid data: @@ -279,7 +304,7 @@ def gen_platform_strings_schema(config: Config, integration: Integration): } """ - def device_class_validator(value): + def device_class_validator(value: str) -> str: """Key validator for platform states. Platform states are only allowed to provide states for device classes they prefix. @@ -313,8 +338,10 @@ ONBOARDING_SCHEMA = vol.Schema({vol.Required("area"): {str: cv.string_with_no_ht def validate_translation_file( # noqa: C901 - config: Config, integration: Integration, all_strings -): + config: Config, + integration: Integration, + all_strings: dict[str, Any] | None, +) -> None: """Validate translation files for integration.""" if config.specific_integrations: check_translations_directory_name(integration) @@ -326,7 +353,7 @@ def validate_translation_file( # noqa: C901 # Only English needs to be always complete strings_files.append(integration.path / "translations/en.json") - references = [] + references: list[dict[str, str]] = [] if integration.domain == "auth": strings_schema = gen_auth_schema(config, integration) @@ -340,6 +367,8 @@ def validate_translation_file( # noqa: C901 ) } ) + elif integration.domain == "homeassistant_hardware": + strings_schema = gen_ha_hardware_schema(config, integration) else: strings_schema = gen_strings_schema(config, integration) @@ -405,6 +434,9 @@ def validate_translation_file( # noqa: C901 if config.specific_integrations: return + if not all_strings: # Nothing to validate against + return + # Validate references for reference in references: parts = reference["ref"].split("::") @@ -421,12 +453,12 @@ def validate_translation_file( # noqa: C901 ) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle JSON files inside integrations.""" if config.specific_integrations: all_strings = None else: - all_strings = upload.generate_upload_data() + all_strings = upload.generate_upload_data() # type: ignore[no-untyped-call] for integration in integrations.values(): validate_translation_file(config, integration, all_strings) diff --git a/script/hassfest/usb.py b/script/hassfest/usb.py index e71966d548a..c5e3148f7b1 100644 --- a/script/hassfest/usb.py +++ b/script/hassfest/usb.py @@ -1,32 +1,16 @@ """Generate usb file.""" from __future__ import annotations -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -USB = {} -""".strip() +from .serializer import format_python_namespace -def generate_and_validate(integrations: list[dict[str, str]]) -> str: +def generate_and_validate(integrations: dict[str, Integration]) -> str: """Validate and generate usb data.""" match_list = [] for domain in sorted(integrations): - integration = integrations[domain] - - if not integration.manifest or not integration.config_flow: - continue - - match_types = integration.manifest.get("usb", []) + match_types = integrations[domain].manifest.get("usb", []) if not match_types: continue @@ -39,7 +23,7 @@ def generate_and_validate(integrations: list[dict[str, str]]) -> str: } ) - return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) + return format_python_namespace({"USB": match_list}) def validate(integrations: dict[str, Integration], config: Config) -> None: diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 939da08319a..76a7be45c34 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -3,36 +3,19 @@ from __future__ import annotations from collections import defaultdict -import black - from homeassistant.loader import async_process_zeroconf_match_dict from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -ZEROCONF = {} - -HOMEKIT = {} -""".strip() +from .serializer import format_python_namespace -def generate_and_validate(integrations: dict[str, Integration]): +def generate_and_validate(integrations: dict[str, Integration]) -> str: """Validate and generate zeroconf data.""" service_type_dict = defaultdict(list) - homekit_dict = {} + homekit_dict: dict[str, str] = {} for domain in sorted(integrations): integration = integrations[domain] - - if not integration.manifest or not integration.config_flow: - continue - service_types = integration.manifest.get("zeroconf", []) homekit = integration.manifest.get("homekit", {}) homekit_models = homekit.get("models", []) @@ -82,15 +65,15 @@ def generate_and_validate(integrations: dict[str, Integration]): warned.add(key_2) break - zeroconf = {key: service_type_dict[key] for key in sorted(service_type_dict)} - homekit = {key: homekit_dict[key] for key in sorted(homekit_dict)} - - return black.format_str( - BASE.format(to_string(zeroconf), to_string(homekit)), mode=black.Mode() + return format_python_namespace( + { + "HOMEKIT": {key: homekit_dict[key] for key in homekit_dict}, + "ZEROCONF": {key: service_type_dict[key] for key in service_type_dict}, + } ) -def validate(integrations: dict[str, Integration], config: Config): +def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate zeroconf file.""" zeroconf_path = config.root / "homeassistant/generated/zeroconf.py" config.cache["zeroconf"] = content = generate_and_validate(integrations) @@ -109,7 +92,7 @@ def validate(integrations: dict[str, Integration], config: Config): return -def generate(integrations: dict[str, Integration], config: Config): +def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate zeroconf file.""" zeroconf_path = config.root / "homeassistant/generated/zeroconf.py" with open(str(zeroconf_path), "w") as fp: diff --git a/script/languages.py b/script/languages.py new file mode 100644 index 00000000000..ad88a31b0b6 --- /dev/null +++ b/script/languages.py @@ -0,0 +1,25 @@ +"""Helper script to update language list from the frontend source.""" +import json +from pathlib import Path +import sys + +import requests + +from .hassfest.serializer import format_python_namespace + +tag = sys.argv[1] if len(sys.argv) > 1 else "dev" + +req = requests.get( + f"https://raw.githubusercontent.com/home-assistant/frontend/{tag}/src/translations/translationMetadata.json" +) +data = json.loads(req.content) +languages = set(data.keys()) + +Path("homeassistant/generated/languages.py").write_text( + format_python_namespace( + { + "LANGUAGES": languages, + }, + generator="script.languages [frontend_tag]", + ) +) diff --git a/script/lint b/script/lint index e4bf74cf602..378c8c68d39 100755 --- a/script/lint +++ b/script/lint @@ -8,7 +8,7 @@ echo '=================================================' echo '= FILES CHANGED =' echo '=================================================' if [ -z "$files" ] ; then - echo "No python file changed. Rather use: tox -e lint\n" + echo "No python file changed.\n" exit fi printf "%s\n" $files diff --git a/script/lazytox.py b/script/lint_and_test.py similarity index 96% rename from script/lazytox.py rename to script/lint_and_test.py index 1f2f4cf02b0..97108e1c630 100755 --- a/script/lazytox.py +++ b/script/lint_and_test.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """ -Lazy 'tox' to quickly check if branch is up to PR standards. +Quickly check if branch is up to PR standards. -This is NOT a tox replacement, only a quick check during development. +This is NOT a full CI/linting replacement, only a quick check during development. """ import asyncio from collections import namedtuple @@ -214,7 +214,7 @@ async def main(): print("=============================") if not test_files: - print("No test files identified, ideally you should run tox") + print("No test files identified") return code, _ = await async_exec( @@ -223,7 +223,7 @@ async def main(): print("=============================") if code == 0: - printc(PASS, "Yay! This will most likely pass tox") + printc(PASS, "Yay! This will most likely pass CI") else: printc(FAIL, "Tests not passing") diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index 015aac2ca91..f9b8e81d364 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -6,6 +6,7 @@ from typing import Any, cast import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( @@ -19,7 +20,7 @@ from .const import DOMAIN OPTIONS_SCHEMA = vol.Schema( { vol.Required(CONF_ENTITY_ID): selector.EntitySelector( - selector.EntitySelectorConfig(domain="sensor") + selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) ), } ) diff --git a/setup.cfg b/setup.cfg index b1a2172f8f1..709b9e4286a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ url = https://www.home-assistant.io/ [flake8] -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +exclude = .venv,.git,docs,venv,bin,lib,deps,build max-complexity = 25 doctests = True # To work with Black diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 49c2c776684..f8ba8108f59 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -28,7 +28,7 @@ from tests.common import ( @pytest.fixture -def mock_hass(loop): +def mock_hass(event_loop): """Home Assistant mock with minimum amount of data set to make it work with auth.""" hass = Mock() hass.config.skip_pip = True diff --git a/tests/common.py b/tests/common.py index 14f3cdd47c2..46022022df6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -22,7 +22,7 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import voluptuous as vol -from homeassistant import auth, config_entries, core as ha, loader +from homeassistant import auth, bootstrap, config_entries, core as ha, loader from homeassistant.auth import ( auth_store, models as auth_models, @@ -160,7 +160,7 @@ def get_test_home_assistant(): # pylint: disable=protected-access -async def async_test_home_assistant(loop, load_registries=True): +async def async_test_home_assistant(event_loop, load_registries=True): """Return a Home Assistant object pointing at test config dir.""" hass = ha.HomeAssistant() store = auth_store.AuthStore(hass) @@ -289,6 +289,7 @@ async def async_test_home_assistant(loop, load_registries=True): hass.config.units = METRIC_SYSTEM hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True + hass.config.skip_pip_packages = [] hass.config_entries = config_entries.ConfigEntries( hass, @@ -306,6 +307,7 @@ async def async_test_home_assistant(loop, load_registries=True): issue_registry.async_load(hass), ) await hass.async_block_till_done() + hass.data[bootstrap.DATA_REGISTRIES_LOADED] = None hass.state = ha.CoreState.running @@ -380,16 +382,58 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @ha.callback -def async_fire_time_changed( - hass: HomeAssistant, datetime_: datetime = None, fire_all: bool = False +def async_fire_time_changed_exact( + hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False ) -> None: - """Fire a time changed event.""" + """Fire a time changed event at an exact microsecond. + + Consider that it is not possible to actually achieve an exact + microsecond in production as the event loop is not precise enough. + If your code relies on this level of precision, consider a different + approach, as this is only for testing. + """ if datetime_ is None: utc_datetime = date_util.utcnow() else: utc_datetime = date_util.as_utc(datetime_) - timestamp = date_util.utc_to_timestamp(utc_datetime) + _async_fire_time_changed(hass, utc_datetime, fire_all) + + +@ha.callback +def async_fire_time_changed( + hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False +) -> None: + """Fire a time changed event. + + This function will add up to 0.5 seconds to the time to ensure that + it accounts for the accidental synchronization avoidance code in repeating + listeners. + + As asyncio is cooperative, we can't guarantee that the event loop will + run an event at the exact time we want. If you need to fire time changed + for an exact microsecond, use async_fire_time_changed_exact. + """ + if datetime_ is None: + utc_datetime = date_util.utcnow() + else: + utc_datetime = date_util.as_utc(datetime_) + + if utc_datetime.microsecond < 500000: + # Allow up to 500000 microseconds to be added to the time + # to handle update_coordinator's and + # async_track_time_interval's + # staggering to avoid thundering herd. + utc_datetime = utc_datetime.replace(microsecond=500000) + + _async_fire_time_changed(hass, utc_datetime, fire_all) + + +@ha.callback +def _async_fire_time_changed( + hass: HomeAssistant, utc_datetime: datetime | None, fire_all: bool +) -> None: + timestamp = date_util.utc_to_timestamp(utc_datetime) for task in list(hass.loop._scheduled): if not isinstance(task, asyncio.TimerHandle): continue diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index b4d804dceae..4abde4d7620 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -172,7 +172,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FORECAST: True} diff --git a/tests/components/airq/__init__.py b/tests/components/airq/__init__.py new file mode 100644 index 00000000000..612761c0653 --- /dev/null +++ b/tests/components/airq/__init__.py @@ -0,0 +1 @@ +"""Tests for the air-Q integration.""" diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py new file mode 100644 index 00000000000..38fc15fdae3 --- /dev/null +++ b/tests/components/airq/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the air-Q config flow.""" +from unittest.mock import patch + +from aioairq.core import DeviceInfo, InvalidAuth, InvalidInput +from aiohttp.client_exceptions import ClientConnectionError + +from homeassistant import config_entries +from homeassistant.components.airq.const import DOMAIN +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +TEST_USER_DATA = { + CONF_IP_ADDRESS: "192.168.0.0", + CONF_PASSWORD: "password", +} +TEST_DEVICE_INFO = DeviceInfo( + id="id", + name="name", + model="model", + sw_version="sw", + hw_version="hw", +) +TEST_DATA_OUT = TEST_USER_DATA | { + "device_info": {k: v for k, v in TEST_DEVICE_INFO.items() if k != "id"} +} + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch("aioairq.AirQ.validate"), patch( + "aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_USER_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == TEST_DEVICE_INFO["name"] + assert result2["data"] == TEST_DATA_OUT + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioairq.AirQ.validate", side_effect=InvalidAuth): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA | {CONF_PASSWORD: "wrong_password"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioairq.AirQ.validate", side_effect=ClientConnectionError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_input(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioairq.AirQ.validate", side_effect=InvalidInput): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA | {CONF_IP_ADDRESS: "invalid_ip"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_input"} diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 32eaade93ee..a4b4c310f38 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aioairzone.const import API_SYSTEMS +from aioairzone.const import API_MAC, API_SYSTEMS from aioairzone.exceptions import ( AirzoneError, InvalidMethod, @@ -10,16 +10,28 @@ from aioairzone.exceptions import ( SystemOutOfRange, ) -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp +from homeassistant.components.airzone.config_flow import short_mac from homeassistant.components.airzone.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT from homeassistant.core import HomeAssistant -from .util import CONFIG, CONFIG_ID1, HVAC_MOCK, HVAC_WEBSERVER_MOCK +from .util import CONFIG, CONFIG_ID1, HVAC_MOCK, HVAC_VERSION_MOCK, HVAC_WEBSERVER_MOCK from tests.common import MockConfigEntry +DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( + hostname="airzone", + ip="192.168.1.100", + macaddress="E84F25000000", +) + +TEST_ID = 1 +TEST_IP = DHCP_SERVICE_INFO.ip +TEST_PORT = 3000 + async def test_form(hass: HomeAssistant) -> None: """Test that the form is served with valid input.""" @@ -145,3 +157,194 @@ async def test_connection_error(hass: HomeAssistant): ) assert result["errors"] == {"base": "cannot_connect"} + + +async def test_dhcp_flow(hass: HomeAssistant) -> None: + """Test that DHCP discovery works.""" + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + + with patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PORT: TEST_PORT, + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + CONF_HOST: TEST_IP, + CONF_PORT: TEST_PORT, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_flow_error(hass: HomeAssistant) -> None: + """Test that DHCP discovery fails.""" + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + side_effect=AirzoneError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_dhcp_connection_error(hass: HomeAssistant): + """Test DHCP connection to host error.""" + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.validate", + side_effect=AirzoneError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PORT: 3001, + }, + ) + + assert result["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PORT: TEST_PORT, + }, + ) + + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + entry = conf_entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"Airzone {short_mac(HVAC_WEBSERVER_MOCK[API_MAC])}" + assert result["data"][CONF_HOST] == TEST_IP + assert result["data"][CONF_PORT] == TEST_PORT + + mock_setup_entry.assert_called_once() + + +async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: + """Test Invalid System ID 0.""" + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + + with patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + side_effect=InvalidSystem, + ) as mock_hvac, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + side_effect=InvalidMethod, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PORT: TEST_PORT, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + assert result["errors"] == {CONF_ID: "invalid_system_id"} + + mock_hvac.return_value = HVAC_MOCK[API_SYSTEMS][0] + mock_hvac.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PORT: TEST_PORT, + CONF_ID: TEST_ID, + }, + ) + + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + entry = conf_entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"Airzone {short_mac(DHCP_SERVICE_INFO.macaddress)}" + assert result["data"][CONF_HOST] == TEST_IP + assert result["data"][CONF_PORT] == TEST_PORT + assert result["data"][CONF_ID] == TEST_ID + + mock_setup_entry.assert_called_once() diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 6b81c493eb6..5bed0fc1d99 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -30,6 +30,7 @@ from aioairzone.const import ( API_THERMOS_RADIO, API_THERMOS_TYPE, API_UNITS, + API_VERSION, API_WIFI_CHANNEL, API_WIFI_RSSI, API_ZONE_ID, @@ -191,6 +192,10 @@ HVAC_SYSTEMS_MOCK = { ] } +HVAC_VERSION_MOCK = { + API_VERSION: "1.62", +} + HVAC_WEBSERVER_MOCK = { API_MAC: "11:22:33:44:55:66", API_WIFI_CHANNEL: 6, diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 608ff428d04..13d095f1cc6 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -411,6 +411,72 @@ async def test_report_fan_speed_state(hass): properties.assert_equal("Alexa.RangeController", "rangeValue", 0) +async def test_report_humidifier_humidity_state(hass): + """Test PercentageController, PowerLevelController reports humidifier humidity correctly.""" + hass.states.async_set( + "humidifier.dry", + "on", + { + "friendly_name": "Humidifier dry", + "supported_features": 0, + "humidity": 25, + "min_humidity": 20, + "max_humidity": 90, + }, + ) + hass.states.async_set( + "humidifier.wet", + "on", + { + "friendly_name": "Humidifier wet", + "supported_features": 0, + "humidity": 80, + "min_humidity": 20, + "max_humidity": 90, + }, + ) + properties = await reported_properties(hass, "humidifier.dry") + properties.assert_equal("Alexa.RangeController", "rangeValue", 25) + + properties = await reported_properties(hass, "humidifier.wet") + properties.assert_equal("Alexa.RangeController", "rangeValue", 80) + + +async def test_report_humidifier_mode(hass): + """Test ModeController reports humidifier mode correctly.""" + hass.states.async_set( + "humidifier.auto", + "on", + { + "friendly_name": "Humidifier auto", + "supported_features": 1, + "humidity": 50, + "mode": "Auto", + "available_modes": ["Auto", "Low", "Medium", "High"], + "min_humidity": 20, + "max_humidity": 90, + }, + ) + properties = await reported_properties(hass, "humidifier.auto") + properties.assert_equal("Alexa.ModeController", "mode", "mode.Auto") + + hass.states.async_set( + "humidifier.medium", + "on", + { + "friendly_name": "Humidifier auto", + "supported_features": 1, + "humidity": 60, + "mode": "Medium", + "available_modes": ["Auto", "Low", "Medium", "High"], + "min_humidity": 20, + "max_humidity": 90, + }, + ) + properties = await reported_properties(hass, "humidifier.medium") + properties.assert_equal("Alexa.ModeController", "mode", "mode.Medium") + + async def test_report_fan_preset_mode(hass): """Test ModeController reports fan preset_mode correctly.""" hass.states.async_set( diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 8b9c91e28b5..499848f01db 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -21,8 +21,9 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, hass_client): +def alexa_client(event_loop, hass, hass_client): """Initialize a Home Assistant server for testing this module.""" + loop = event_loop @callback def mock_service(call): diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 9c71bc32e4d..54708e9d0f0 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -27,8 +27,9 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, hass_client): +def alexa_client(event_loop, hass, hass_client): """Initialize a Home Assistant server for testing this module.""" + loop = event_loop @callback def mock_service(call): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index e2ae8741f20..3b76654f312 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -950,6 +950,145 @@ async def test_single_preset_mode_fan(hass, caplog): caplog.clear() +@freeze_time("2022-04-19 07:53:05") +async def test_humidifier(hass, caplog): + """Test humidifier controller.""" + device = ( + "humidifier.test_1", + "on", + { + "friendly_name": "Humidifier test 1", + "humidity": 66, + "supported_features": 1, + "mode": "Auto", + "available_modes": ["Auto", "Low", "Medium", "High"], + "min_humidity": 20, + "max_humidity": 90, + }, + ) + await discovery_test(device, hass) + + await assert_power_controller_works( + "humidifier#test_1", + "humidifier.turn_on", + "humidifier.turn_off", + hass, + "2022-04-19T07:53:05Z", + ) + + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "humidifier#test_1", + "humidifier.set_mode", + hass, + payload={"mode": "mode.Auto"}, + instance="humidifier.mode", + ) + assert call.data["mode"] == "Auto" + + with pytest.raises(AssertionError): + await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "humidifier#test_1", + "humidifier.set_mode", + hass, + payload={"mode": "mode.-"}, + instance="humidifier.mode", + ) + assert "Entity 'humidifier.test_1' does not support Mode '-'" in caplog.text + caplog.clear() + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "humidifier#test_1", + "humidifier.set_humidity", + hass, + payload={"rangeValue": "67"}, + instance="humidifier.humidity", + ) + assert call.data["humidity"] == 67 + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "humidifier#test_1", + "humidifier.set_humidity", + hass, + payload={"rangeValue": "33"}, + instance="humidifier.humidity", + ) + assert call.data["humidity"] == 33 + + +async def test_humidifier_without_modes(hass): + """Test humidifier discovery without modes.""" + + device = ( + "humidifier.test_2", + "on", + { + "friendly_name": "Humidifier test 2", + "humidity": 33, + "supported_features": 0, + "min_humidity": 20, + "max_humidity": 90, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "humidifier#test_2" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Humidifier test 2" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.RangeController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + "Alexa", + ) + + power_capability = get_capability(capabilities, "Alexa.PowerController") + assert "capabilityResources" not in power_capability + assert "configuration" not in power_capability + + +async def test_humidifier_with_modes(hass): + """Test humidifier discovery with modes.""" + + device = ( + "humidifier.test_1", + "on", + { + "friendly_name": "Humidifier test 1", + "humidity": 66, + "supported_features": 1, + "mode": "Auto", + "available_modes": ["Auto", "Low", "Medium", "High"], + "min_humidity": 20, + "max_humidity": 90, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "humidifier#test_1" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Humidifier test 1" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.ModeController", + "Alexa.RangeController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + "Alexa", + ) + + power_capability = get_capability(capabilities, "Alexa.PowerController") + assert "capabilityResources" not in power_capability + assert "configuration" not in power_capability + + async def test_lock(hass): """Test lock discovery.""" device = ("lock.test", "off", {"friendly_name": "Test lock"}) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index bb61cea2413..ed70afc02d6 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -209,8 +209,8 @@ async def test_report_state_unsets_authorized_on_access_token_error( config._store.set_authorized.assert_called_once_with(False) -async def test_report_state_instance(hass, aioclient_mock): - """Test proactive state reports with instance.""" +async def test_report_state_fan(hass, aioclient_mock): + """Test proactive state reports with fan instance.""" aioclient_mock.post(TEST_URL, text="", status=202) hass.states.async_set( @@ -275,6 +275,64 @@ async def test_report_state_instance(hass, aioclient_mock): assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan" +async def test_report_state_humidifier(hass, aioclient_mock): + """Test proactive state reports with humidifier instance.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + hass.states.async_set( + "humidifier.test_humidifier", + "off", + { + "friendly_name": "Test humidifier", + "supported_features": 1, + "mode": None, + "available_modes": ["auto", "smart"], + }, + ) + + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) + + hass.states.async_set( + "humidifier.test_humidifier", + "on", + { + "friendly_name": "Test humidifier", + "supported_features": 1, + "mode": "smart", + "available_modes": ["auto", "smart"], + "humidity": 55, + }, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa" + assert call_json["event"]["header"]["name"] == "ChangeReport" + + change_reports = call_json["event"]["payload"]["change"]["properties"] + + checks = 0 + for report in change_reports: + if report["name"] == "mode": + assert report["value"] == "mode.smart" + assert report["instance"] == "humidifier.mode" + assert report["namespace"] == "Alexa.ModeController" + checks += 1 + if report["name"] == "rangeValue": + assert report["value"] == 55 + assert report["instance"] == "humidifier.humidity" + assert report["namespace"] == "Alexa.RangeController" + checks += 1 + assert checks == 2 + + assert call_json["event"]["endpoint"]["endpointId"] == "humidifier#test_humidifier" + + async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" aioclient_mock.post(TEST_URL, text="") diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index d56389c7230..d6bfd3de521 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -327,35 +327,35 @@ async def test_stream(hass, mock_api_client): """Test the stream.""" listen_count = _listen_count(hass) - resp = await mock_api_client.get(const.URL_API_STREAM) - assert resp.status == HTTPStatus.OK - assert listen_count + 1 == _listen_count(hass) + async with mock_api_client.get(const.URL_API_STREAM) as resp: + assert resp.status == HTTPStatus.OK + assert listen_count + 1 == _listen_count(hass) - hass.bus.async_fire("test_event") + hass.bus.async_fire("test_event") - data = await _stream_next_event(resp.content) + data = await _stream_next_event(resp.content) - assert data["event_type"] == "test_event" + assert data["event_type"] == "test_event" async def test_stream_with_restricted(hass, mock_api_client): """Test the stream with restrictions.""" listen_count = _listen_count(hass) - resp = await mock_api_client.get( + async with mock_api_client.get( f"{const.URL_API_STREAM}?restrict=test_event1,test_event3" - ) - assert resp.status == HTTPStatus.OK - assert listen_count + 1 == _listen_count(hass) + ) as resp: + assert resp.status == HTTPStatus.OK + assert listen_count + 1 == _listen_count(hass) - hass.bus.async_fire("test_event1") - data = await _stream_next_event(resp.content) - assert data["event_type"] == "test_event1" + hass.bus.async_fire("test_event1") + data = await _stream_next_event(resp.content) + assert data["event_type"] == "test_event1" - hass.bus.async_fire("test_event2") - hass.bus.async_fire("test_event3") - data = await _stream_next_event(resp.content) - assert data["event_type"] == "test_event3" + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + data = await _stream_next_event(resp.content) + assert data["event_type"] == "test_event3" async def _stream_next_event(stream): diff --git a/tests/components/aranet/__init__.py b/tests/components/aranet/__init__.py new file mode 100644 index 00000000000..2fe27329bda --- /dev/null +++ b/tests/components/aranet/__init__.py @@ -0,0 +1,58 @@ +"""Tests for the Aranet integration.""" + +from time import time + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + + +def fake_service_info(name, service_uuid, manufacturer_data): + """Return a BluetoothServiceInfoBleak for use in testing.""" + return BluetoothServiceInfoBleak( + name=name, + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + manufacturer_data=manufacturer_data, + service_data={}, + service_uuids=[service_uuid], + source="local", + connectable=False, + time=time(), + device=BLEDevice("aa:bb:cc:dd:ee:ff", name=name), + advertisement=AdvertisementData( + local_name=name, + manufacturer_data=manufacturer_data, + service_data={}, + service_uuids=[service_uuid], + rssi=-60, + tx_power=-127, + platform_data=(), + ), + ) + + +NOT_ARANET4_SERVICE_INFO = fake_service_info( + "Not it", "61DE521B-F0BF-9F44-64D4-75BBE1738105", {3234: b"\x00\x01"} +) + +OLD_FIRMWARE_SERVICE_INFO = fake_service_info( + "Aranet4 12345", + "f0cd1400-95da-4f4b-9ac8-aa55d312af0c", + {1794: b"\x21\x0a\x04\x00\x00\x00\x00\x00"}, +) + +DISABLED_INTEGRATIONS_SERVICE_INFO = fake_service_info( + "Aranet4 12345", + "0000fce0-0000-1000-8000-00805f9b34fb", + {1794: b"\x01\x00\x02\x01\x00\x00\x00\x00"}, +) + +VALID_DATA_SERVICE_INFO = fake_service_info( + "Aranet4 12345", + "0000fce0-0000-1000-8000-00805f9b34fb", + { + 1794: b'\x21\x00\x02\x01\x00\x00\x00\x01\x8a\x02\xa5\x01\xb1&"Y\x01,\x01\xe8\x00\x88' + }, +) diff --git a/tests/components/aranet/conftest.py b/tests/components/aranet/conftest.py new file mode 100644 index 00000000000..fca081d2e2a --- /dev/null +++ b/tests/components/aranet/conftest.py @@ -0,0 +1,8 @@ +"""Aranet session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/aranet/test_config_flow.py b/tests/components/aranet/test_config_flow.py new file mode 100644 index 00000000000..2b4172c30cd --- /dev/null +++ b/tests/components/aranet/test_config_flow.py @@ -0,0 +1,252 @@ +"""Test the Aranet config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.aranet.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + DISABLED_INTEGRATIONS_SERVICE_INFO, + NOT_ARANET4_SERVICE_INFO, + OLD_FIRMWARE_SERVICE_INFO, + VALID_DATA_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet4 12345" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_aranet4(hass): + """Test that we reject discovery via Bluetooth for an unrelated device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_ARANET4_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet4 12345" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_user_no_devices_found(hass: HomeAssistant): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_only_other_devices_found(hass: HomeAssistant): + """Test setup from service info cache with only other devices found.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[NOT_ARANET4_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass: HomeAssistant): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet4 12345" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass: HomeAssistant): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass: HomeAssistant): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_old_firmware(hass: HomeAssistant): + """Test we can't set up a device with firmware too old to report measurements.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[OLD_FIRMWARE_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "outdated_version" + + +async def test_async_step_user_integrations_disabled(hass: HomeAssistant): + """Test we can't set up a device the device's integration setting disabled.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[DISABLED_INTEGRATIONS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "integrations_disabled" diff --git a/tests/components/aranet/test_sensor.py b/tests/components/aranet/test_sensor.py new file mode 100644 index 00000000000..a0edcca4803 --- /dev/null +++ b/tests/components/aranet/test_sensor.py @@ -0,0 +1,111 @@ +"""Test the Aranet sensors.""" + + +from homeassistant.components.aranet.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import DISABLED_INTEGRATIONS_SERVICE_INFO, VALID_DATA_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, VALID_DATA_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + batt_sensor = hass.states.get("sensor.aranet4_12345_battery") + batt_sensor_attrs = batt_sensor.attributes + assert batt_sensor.state == "89" + assert batt_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Battery" + assert batt_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert batt_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + co2_sensor = hass.states.get("sensor.aranet4_12345_carbon_dioxide") + co2_sensor_attrs = co2_sensor.attributes + assert co2_sensor.state == "650" + assert co2_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Carbon Dioxide" + assert co2_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "ppm" + assert co2_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + humid_sensor = hass.states.get("sensor.aranet4_12345_humidity") + humid_sensor_attrs = humid_sensor.attributes + assert humid_sensor.state == "34" + assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Humidity" + assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.aranet4_12345_temperature") + temp_sensor_attrs = temp_sensor.attributes + assert temp_sensor.state == "21.1" + assert temp_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Temperature" + assert temp_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + press_sensor = hass.states.get("sensor.aranet4_12345_pressure") + press_sensor_attrs = press_sensor.attributes + assert press_sensor.state == "990.5" + assert press_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Pressure" + assert press_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "hPa" + assert press_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + interval_sensor = hass.states.get("sensor.aranet4_12345_update_interval") + interval_sensor_attrs = interval_sensor.attributes + assert interval_sensor.state == "300" + assert interval_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Update Interval" + assert interval_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "s" + assert interval_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_smart_home_integration_disabled(hass): + """Test disabling smart home integration marks entities as unavailable.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, DISABLED_INTEGRATIONS_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + batt_sensor = hass.states.get("sensor.aranet4_12345_battery") + assert batt_sensor.state == "unavailable" + + co2_sensor = hass.states.get("sensor.aranet4_12345_carbon_dioxide") + assert co2_sensor.state == "unavailable" + + humid_sensor = hass.states.get("sensor.aranet4_12345_humidity") + assert humid_sensor.state == "unavailable" + + temp_sensor = hass.states.get("sensor.aranet4_12345_temperature") + assert temp_sensor.state == "unavailable" + + press_sensor = hass.states.get("sensor.aranet4_12345_pressure") + assert press_sensor.state == "unavailable" + + interval_sensor = hass.states.get("sensor.aranet4_12345_update_interval") + assert interval_sensor.state == "unavailable" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index eeb5adbc7e3..e49775f00b5 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Arcam FMJ config flow module.""" +from dataclasses import replace from unittest.mock import AsyncMock, patch from arcam.fmj.client import ConnectionFailed @@ -107,6 +108,21 @@ async def test_ssdp_unable_to_connect(hass, dummy_client): assert result["reason"] == "cannot_connect" +async def test_ssdp_invalid_id(hass, dummy_client): + """Test a ssdp with invalid UDN.""" + discover = replace( + MOCK_DISCOVER, upnp=MOCK_DISCOVER.upnp | {ssdp.ATTR_UPNP_UDN: "invalid"} + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discover, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + async def test_ssdp_update(hass): """Test a ssdp import flow.""" entry = MockConfigEntry( diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 19a0f456d7f..6a53b70d9e7 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ATTR_NAME, ) -from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_UUID +from .conftest import MOCK_HOST, MOCK_UUID MOCK_TURN_ON = { "service": "switch.turn_on", @@ -41,7 +41,6 @@ async def test_properties(player, state): ATTR_NAME: f"Arcam FMJ ({MOCK_HOST})", ATTR_IDENTIFIERS: { ("arcam_fmj", MOCK_UUID), - ("arcam_fmj", MOCK_HOST, MOCK_PORT), }, ATTR_MODEL: "Arcam FMJ AVR", ATTR_MANUFACTURER: "Arcam", @@ -102,7 +101,8 @@ async def test_mute_volume(player, state, mute): async def test_name(player): """Test name.""" - assert player.name == f"{MOCK_NAME} - Zone: 1" + data = await update(player) + assert data.attributes["friendly_name"] == "Zone 1" async def test_update(player, state): diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 22a780fc12e..f9af800166a 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -248,8 +249,8 @@ async def test_on_connect_failed(hass, side_effect, error): assert result["errors"] == {"base": error} -async def test_options_flow(hass): - """Test config flow options.""" +async def test_options_flow_ap(hass: HomeAssistant) -> None: + """Test config flow options for ap mode.""" config_entry = MockConfigEntry( domain=DOMAIN, data=CONFIG_DATA, @@ -264,6 +265,7 @@ async def test_options_flow(hass): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" + assert CONF_REQUIRE_IP in result["data_schema"].schema result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -282,3 +284,37 @@ async def test_options_flow(hass): assert config_entry.options[CONF_INTERFACE] == "aaa" assert config_entry.options[CONF_DNSMASQ] == "bbb" assert config_entry.options[CONF_REQUIRE_IP] is False + + +async def test_options_flow_router(hass: HomeAssistant) -> None: + """Test config flow options for router mode.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={**CONFIG_DATA, CONF_MODE: "router"}, + ) + config_entry.add_to_hass(hass) + + with PATCH_SETUP_ENTRY: + 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) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert CONF_REQUIRE_IP not in result["data_schema"].schema + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CONSIDER_HOME: 20, + CONF_TRACK_UNKNOWN: True, + CONF_INTERFACE: "aaa", + CONF_DNSMASQ: "bbb", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options[CONF_CONSIDER_HOME] == 20 + assert config_entry.options[CONF_TRACK_UNKNOWN] is True + assert config_entry.options[CONF_INTERFACE] == "aaa" + assert config_entry.options[CONF_DNSMASQ] == "bbb" diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index a4bc3d7b16f..ef82efae177 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -332,7 +332,7 @@ async def test_load_triggers_ble_discovery(hass): august_lock_without_key = await _mock_operative_august_lock_detail(hass) with patch( - "homeassistant.components.august.yalexs_ble.async_discovery" + "homeassistant.components.august.discovery_flow.async_create_flow" ) as mock_discovery: config_entry = await _create_august_with_devices( hass, [august_lock_with_key, august_lock_without_key] @@ -341,7 +341,7 @@ async def test_load_triggers_ble_discovery(hass): assert config_entry.state is ConfigEntryState.LOADED assert len(mock_discovery.mock_calls) == 1 - assert mock_discovery.mock_calls[0][1][1] == { + assert mock_discovery.mock_calls[0].kwargs["data"] == { "name": "Front Door Lock", "address": None, "serial": "X2FSW05DGA", diff --git a/tests/components/auth/conftest.py b/tests/components/auth/conftest.py index 867f44d9f15..b7e69a44a0d 100644 --- a/tests/components/auth/conftest.py +++ b/tests/components/auth/conftest.py @@ -3,6 +3,6 @@ import pytest @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index f40309bf7f6..1ce6cb616da 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -738,7 +738,8 @@ async def test_automation_stops(hass, calls, service): assert len(calls) == (1 if service == "turn_off_no_stop" else 0) -async def test_reload_unchanged_does_not_stop(hass, calls): +@pytest.mark.parametrize("extra_config", ({}, {"id": "sun"})) +async def test_reload_unchanged_does_not_stop(hass, calls, extra_config): """Test that reloading stops any running actions as appropriate.""" test_entity = "test.entity" @@ -753,6 +754,7 @@ async def test_reload_unchanged_does_not_stop(hass, calls): ], } } + config[automation.DOMAIN].update(**extra_config) assert await async_setup_component(hass, automation.DOMAIN, config) running = asyncio.Event() @@ -970,6 +972,41 @@ async def test_reload_identical_automations_without_id(hass, calls): }, } }, + { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "test.automation"}], + }, + # An automation using templates + { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "{{ 'test.automation' }}"}], + }, + # An automation using blueprint + { + "id": "sun", + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + }, + }, + # An automation using blueprint with templated input + { + "id": "sun", + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "{{ 'test_event' }}", + "service_to_call": "{{ 'test.automation' }}", + "a_number": 5, + }, + }, + }, ), ) async def test_reload_unchanged_automation(hass, calls, automation_config): @@ -1004,7 +1041,8 @@ async def test_reload_unchanged_automation(hass, calls, automation_config): assert len(calls) == 2 -async def test_reload_automation_when_blueprint_changes(hass, calls): +@pytest.mark.parametrize("extra_config", ({}, {"id": "sun"})) +async def test_reload_automation_when_blueprint_changes(hass, calls, extra_config): """Test an automation is updated at reload if the blueprint has changed.""" with patch( "homeassistant.components.automation.AutomationEntity", wraps=AutomationEntity @@ -1023,6 +1061,7 @@ async def test_reload_automation_when_blueprint_changes(hass, calls): } ] } + config[automation.DOMAIN][0].update(**extra_config) assert await async_setup_component(hass, automation.DOMAIN, config) assert automation_entity_init.call_count == 1 automation_entity_init.reset_mock() @@ -2101,12 +2140,12 @@ async def test_recursive_automation_starting_script( hass.bus.async_listen("automation_triggered", async_automation_triggered) hass.bus.async_fire("trigger_automation") - await asyncio.wait_for(script_done_event.wait(), 1) + await asyncio.wait_for(script_done_event.wait(), 10) # Trigger 1st stage script shutdown hass.state = CoreState.stopping hass.bus.async_fire("homeassistant_stop") - await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 10) # Trigger 2nd stage script shutdown async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 999204a2d91..9d5d92f8dd2 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -6,6 +6,7 @@ from blinkpy.blinkpy import BlinkSetupError from homeassistant import config_entries, data_entry_flow from homeassistant.components.blink import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -254,7 +255,7 @@ async def test_reauth_shows_user_step(hass): assert result["step_id"] == "user" -async def test_options_flow(hass): +async def test_options_flow(hass: HomeAssistant) -> None: """Test config flow options.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -283,11 +284,16 @@ async def test_options_flow(hass): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "simple_options" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"scan_interval": 5}, - ) + with patch("homeassistant.components.blink.Auth", return_value=mock_auth), patch( + "homeassistant.components.blink.Blink", return_value=mock_blink + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"scan_interval": 5}, + ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == {"scan_interval": 5} - assert mock_blink.refresh_rate == 5 + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {"scan_interval": 5} + await hass.async_block_till_done() + + assert mock_blink.refresh_rate == 5 diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index e695f18c42f..e36d1d4b644 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -5,15 +5,18 @@ import time from typing import Any from unittest.mock import patch +from bleak import BleakClient from bleak.backends.scanner import AdvertisementData, BLEDevice +from bluetooth_adapters import DEFAULT_ADDRESS from homeassistant.components.bluetooth import ( DOMAIN, SOURCE_LOCAL, + BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_get_advertisement_callback, models, ) -from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS from homeassistant.components.bluetooth.manager import BluetoothManager from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -29,6 +32,7 @@ __all__ = ( "patch_all_discovered_devices", "patch_discovered_devices", "generate_advertisement_data", + "MockBleakClient", ) ADVERTISEMENT_DATA_DEFAULTS = { @@ -94,7 +98,7 @@ def inject_advertisement_with_time_and_source_connectable( ) -> None: """Inject an advertisement into the manager from a specific source at a time and connectable status.""" async_get_advertisement_callback(hass)( - models.BluetoothServiceInfoBleak( + BluetoothServiceInfoBleak( name=adv.local_name or device.name or device.address, address=device.address, rssi=adv.rssi, @@ -111,7 +115,7 @@ def inject_advertisement_with_time_and_source_connectable( def inject_bluetooth_service_info_bleak( - hass: HomeAssistant, info: models.BluetoothServiceInfoBleak + hass: HomeAssistant, info: BluetoothServiceInfoBleak ) -> None: """Inject an advertisement into the manager with connectable status.""" advertisement_data = generate_advertisement_data( @@ -137,7 +141,7 @@ def inject_bluetooth_service_info_bleak( def inject_bluetooth_service_info( - hass: HomeAssistant, info: models.BluetoothServiceInfo + hass: HomeAssistant, info: BluetoothServiceInfo ) -> None: """Inject a BluetoothServiceInfo into the manager.""" advertisement_data = generate_advertisement_data( # type: ignore[no-untyped-call] @@ -190,3 +194,32 @@ async def _async_setup_with_adapter( assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() return entry + + +class MockBleakClient(BleakClient): + """Mock bleak client.""" + + def __init__(self, *args, **kwargs): + """Mock init.""" + super().__init__(*args, **kwargs) + self._device_path = "/dev/test" + + @property + def is_connected(self) -> bool: + """Mock connected.""" + return True + + async def connect(self, *args, **kwargs): + """Mock connect.""" + return True + + async def disconnect(self, *args, **kwargs): + """Mock disconnect.""" + + async def get_services(self, *args, **kwargs): + """Mock get_services.""" + return [] + + async def clear_cache(self, *args, **kwargs): + """Mock clear_cache.""" + return True diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 3d29b4cbbe1..a6593adef49 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,6 +1,6 @@ """Tests for the bluetooth component.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import patch import pytest @@ -43,16 +43,6 @@ def mock_operating_system_90(): yield -@pytest.fixture(name="bluez_dbus_mock") -def bluez_dbus_mock(): - """Fixture that mocks out the bluez dbus calls.""" - # Must patch directly since this is loaded on demand only - with patch( - "bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock()) - ): - yield - - @pytest.fixture(name="macos_adapter") def macos_adapter(): """Fixture that mocks the macos adapter.""" @@ -62,7 +52,7 @@ def macos_adapter(): "homeassistant.components.bluetooth.scanner.platform.system", return_value="Darwin", ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" + "bluetooth_adapters.systems.platform.system", return_value="Darwin" ): yield @@ -71,14 +61,14 @@ def macos_adapter(): def windows_adapter(): """Fixture that mocks the windows adapter.""" with patch( - "homeassistant.components.bluetooth.util.platform.system", + "bluetooth_adapters.systems.platform.system", return_value="Windows", ): yield @pytest.fixture(name="no_adapters") -def no_adapter_fixture(bluez_dbus_mock): +def no_adapter_fixture(): """Fixture that mocks no adapters on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" @@ -86,16 +76,18 @@ def no_adapter_fixture(bluez_dbus_mock): "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + "bluetooth_adapters.systems.platform.system", return_value="Linux" ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={}, + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" + ), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + {}, ): yield @pytest.fixture(name="one_adapter") -def one_adapter_fixture(bluez_dbus_mock): +def one_adapter_fixture(): """Fixture that mocks one adapter on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" @@ -103,20 +95,21 @@ def one_adapter_fixture(bluez_dbus_mock): "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + "bluetooth_adapters.systems.platform.system", return_value="Linux" ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={ + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" + ), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { "hci0": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:01", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - }, - "org.bluez.AdvertisementMonitorManager1": { - "SupportedMonitorTypes": ["or_patterns"], - "SupportedFeatures": [], - }, + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": True, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, }, ): @@ -124,7 +117,7 @@ def one_adapter_fixture(bluez_dbus_mock): @pytest.fixture(name="two_adapters") -def two_adapters_fixture(bluez_dbus_mock): +def two_adapters_fixture(): """Fixture that mocks two adapters on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" @@ -132,27 +125,31 @@ def two_adapters_fixture(bluez_dbus_mock): "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + "bluetooth_adapters.systems.platform.system", return_value="Linux" ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={ + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" + ), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { "hci0": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:01", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - } + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, "hci1": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:02", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - }, - "org.bluez.AdvertisementMonitorManager1": { - "SupportedMonitorTypes": ["or_patterns"], - "SupportedFeatures": [], - }, + "address": "00:00:00:00:00:02", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": True, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, }, ): @@ -160,7 +157,7 @@ def two_adapters_fixture(bluez_dbus_mock): @pytest.fixture(name="one_adapter_old_bluez") -def one_adapter_old_bluez(bluez_dbus_mock): +def one_adapter_old_bluez(): """Fixture that mocks two adapters on Linux.""" with patch( "homeassistant.components.bluetooth.platform.system", return_value="Linux" @@ -168,15 +165,21 @@ def one_adapter_old_bluez(bluez_dbus_mock): "homeassistant.components.bluetooth.scanner.platform.system", return_value="Linux", ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + "bluetooth_adapters.systems.platform.system", return_value="Linux" ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={ + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" + ), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { "hci0": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:01", - "Name": "BlueZ 4.43", - } + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, }, ): diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py index 6eb2b5a968e..29787f92910 100644 --- a/tests/components/bluetooth/test_advertisement_tracker.py +++ b/tests/components/bluetooth/test_advertisement_tracker.py @@ -7,6 +7,7 @@ from unittest.mock import patch from bleak.backends.scanner import AdvertisementData, BLEDevice from homeassistant.components.bluetooth import ( + BaseHaScanner, async_register_scanner, async_track_unavailable, ) @@ -17,7 +18,6 @@ from homeassistant.components.bluetooth.const import ( SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) -from homeassistant.components.bluetooth.models import BaseHaScanner from homeassistant.core import callback from homeassistant.util import dt as dt_util @@ -314,7 +314,7 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c """Return a list of discovered devices.""" return {} - scanner = FakeScanner(hass, "new") + scanner = FakeScanner(hass, "new", "fake_adapter") cancel_scanner = async_register_scanner(hass, scanner, False) @callback diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py new file mode 100644 index 00000000000..b1d7e68972b --- /dev/null +++ b/tests/components/bluetooth/test_api.py @@ -0,0 +1,16 @@ +"""Tests for the Bluetooth integration API.""" + + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import BaseHaScanner, async_scanner_by_source + + +async def test_scanner_by_source(hass, enable_bluetooth): + """Test we can get a scanner by source.""" + + hci2_scanner = BaseHaScanner(hass, "hci2", "hci2") + cancel_hci2 = bluetooth.async_register_scanner(hass, hci2_scanner, True) + + assert async_scanner_by_source(hass, "hci2") is hci2_scanner + cancel_hci2() + assert async_scanner_by_source(hass, "hci2") is None diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py new file mode 100644 index 00000000000..19c47361a6a --- /dev/null +++ b/tests/components/bluetooth/test_base_scanner.py @@ -0,0 +1,329 @@ +"""Tests for the Bluetooth base scanner models.""" +from __future__ import annotations + +from datetime import timedelta +import time +from unittest.mock import patch + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BaseHaRemoteScanner, HaBluetoothConnector +from homeassistant.components.bluetooth.const import ( + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) +import homeassistant.util.dt as dt_util + +from . import MockBleakClient, _get_manager, generate_advertisement_data + +from tests.common import async_fire_time_changed + + +async def test_remote_scanner(hass, enable_bluetooth): + """Test the remote scanner base class merges advertisement_data.""" + manager = _get_manager() + + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"], + service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + switchbot_device_2 = BLEDevice( + "44:44:33:11:23:45", + "w", + {}, + rssi=-100, + ) + switchbot_device_adv_2 = generate_advertisement_data( + local_name="wohand", + service_uuids=["00000001-0000-1000-8000-00805f9b34fb"], + service_data={"00000001-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01", 2: b"\x02"}, + rssi=-100, + ) + + class FakeScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True) + scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + + scanner.inject_advertisement(switchbot_device, switchbot_device_adv) + + data = scanner.discovered_devices_and_advertisement_data + discovered_device, discovered_adv_data = data[switchbot_device.address] + assert discovered_device.address == switchbot_device.address + assert discovered_device.name == switchbot_device.name + assert ( + discovered_adv_data.manufacturer_data == switchbot_device_adv.manufacturer_data + ) + assert discovered_adv_data.service_data == switchbot_device_adv.service_data + assert discovered_adv_data.service_uuids == switchbot_device_adv.service_uuids + scanner.inject_advertisement(switchbot_device_2, switchbot_device_adv_2) + + data = scanner.discovered_devices_and_advertisement_data + discovered_device, discovered_adv_data = data[switchbot_device.address] + assert discovered_device.address == switchbot_device.address + assert discovered_device.name == switchbot_device.name + assert discovered_adv_data.manufacturer_data == {1: b"\x01", 2: b"\x02"} + assert discovered_adv_data.service_data == { + "050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff", + "00000001-0000-1000-8000-00805f9b34fb": b"\n\xff", + } + assert set(discovered_adv_data.service_uuids) == { + "050a021a-0000-1000-8000-00805f9b34fb", + "00000001-0000-1000-8000-00805f9b34fb", + } + + cancel() + + +async def test_remote_scanner_expires_connectable(hass, enable_bluetooth): + """Test the remote scanner expires stale connectable data.""" + manager = _get_manager() + + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=[], + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True) + scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + + start_time_monotonic = time.monotonic() + scanner.inject_advertisement(switchbot_device, switchbot_device_adv) + + devices = scanner.discovered_devices + assert len(scanner.discovered_devices) == 1 + assert len(scanner.discovered_devices_and_advertisement_data) == 1 + assert devices[0].name == "wohand" + + expire_monotonic = ( + start_time_monotonic + + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + + 1 + ) + expire_utc = dt_util.utcnow() + timedelta( + seconds=CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + ) + with patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=expire_monotonic, + ): + async_fire_time_changed(hass, expire_utc) + await hass.async_block_till_done() + + devices = scanner.discovered_devices + assert len(scanner.discovered_devices) == 0 + assert len(scanner.discovered_devices_and_advertisement_data) == 0 + + cancel() + + +async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth): + """Test the remote scanner expires stale non connectable data.""" + manager = _get_manager() + + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=[], + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + + start_time_monotonic = time.monotonic() + scanner.inject_advertisement(switchbot_device, switchbot_device_adv) + + devices = scanner.discovered_devices + assert len(scanner.discovered_devices) == 1 + assert len(scanner.discovered_devices_and_advertisement_data) == 1 + assert devices[0].name == "wohand" + + assert ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + > CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) + + # The connectable timeout is not used for non connectable devices + expire_monotonic = ( + start_time_monotonic + + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + + 1 + ) + expire_utc = dt_util.utcnow() + timedelta( + seconds=CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + ) + with patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=expire_monotonic, + ): + async_fire_time_changed(hass, expire_utc) + await hass.async_block_till_done() + + assert len(scanner.discovered_devices) == 1 + assert len(scanner.discovered_devices_and_advertisement_data) == 1 + + # The non connectable timeout is used for non connectable devices + # which is always longer than the connectable timeout + expire_monotonic = ( + start_time_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + ) + expire_utc = dt_util.utcnow() + timedelta( + seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + ) + with patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=expire_monotonic, + ): + async_fire_time_changed(hass, expire_utc) + await hass.async_block_till_done() + + assert len(scanner.discovered_devices) == 0 + assert len(scanner.discovered_devices_and_advertisement_data) == 0 + + cancel() + + +async def test_base_scanner_connecting_behavior(hass, enable_bluetooth): + """Test that the default behavior is to mark the scanner as not scanning when connecting.""" + manager = _get_manager() + + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=[], + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + + with scanner.connecting(): + assert scanner.scanning is False + + # We should still accept new advertisements while connecting + # since advertisements are delivered asynchronously and + # we don't want to miss any even when we are willing to + # accept advertisements from another scanner in the brief window + # between when we start connecting and when we stop scanning + scanner.inject_advertisement(switchbot_device, switchbot_device_adv) + + devices = scanner.discovered_devices + assert len(scanner.discovered_devices) == 1 + assert len(scanner.discovered_devices_and_advertisement_data) == 1 + assert devices[0].name == "wohand" + + cancel() diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 69619ba76a7..9219d37a399 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -2,14 +2,14 @@ from unittest.mock import patch +from bluetooth_adapters import DEFAULT_ADDRESS, AdapterDetails + from homeassistant import config_entries from homeassistant.components.bluetooth.const import ( CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, - DEFAULT_ADDRESS, DOMAIN, - AdapterDetails, ) from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -130,7 +130,10 @@ async def test_async_step_integration_discovery(hass): """Test setting up from integration discovery.""" details = AdapterDetails( - address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + address="00:00:00:00:00:01", + sw_version="1.23.5", + hw_version="1.2.3", + manufacturer="ACME", ) result = await hass.config_entries.flow.async_init( @@ -159,7 +162,10 @@ async def test_async_step_integration_discovery_during_onboarding_one_adapter( ): """Test setting up from integration discovery during onboarding.""" details = AdapterDetails( - address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + address="00:00:00:00:00:01", + sw_version="1.23.5", + hw_version="1.2.3", + manufacturer="ACME", ) with patch( @@ -187,10 +193,16 @@ async def test_async_step_integration_discovery_during_onboarding_two_adapters( ): """Test setting up from integration discovery during onboarding.""" details1 = AdapterDetails( - address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + address="00:00:00:00:00:01", + sw_version="1.23.5", + hw_version="1.2.3", + manufacturer="ACME", ) details2 = AdapterDetails( - address="00:00:00:00:00:02", sw_version="1.23.5", hw_version="1.2.3" + address="00:00:00:00:00:02", + sw_version="1.23.5", + hw_version="1.2.3", + manufacturer="ACME", ) with patch( @@ -226,7 +238,10 @@ async def test_async_step_integration_discovery_during_onboarding_two_adapters( async def test_async_step_integration_discovery_during_onboarding(hass, macos_adapter): """Test setting up from integration discovery during onboarding.""" details = AdapterDetails( - address=DEFAULT_ADDRESS, sw_version="1.23.5", hw_version="1.2.3" + address=DEFAULT_ADDRESS, + sw_version="1.23.5", + hw_version="1.2.3", + manufacturer="ACME", ) with patch( @@ -252,7 +267,10 @@ async def test_async_step_integration_discovery_during_onboarding(hass, macos_ad async def test_async_step_integration_discovery_already_exists(hass): """Test setting up from integration discovery when an entry already exists.""" details = AdapterDetails( - address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + address="00:00:00:00:00:01", + sw_version="1.23.5", + hw_version="1.2.3", + manufacturer="ACME", ) entry = MockConfigEntry(domain=DOMAIN, unique_id="00:00:00:00:00:01") diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index a8d4d7aa142..417375e9820 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -4,9 +4,9 @@ from unittest.mock import ANY, patch from bleak.backends.scanner import BLEDevice +from bluetooth_adapters import DEFAULT_ADDRESS from homeassistant.components import bluetooth -from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS from . import generate_advertisement_data, inject_advertisement @@ -24,8 +24,13 @@ async def test_diagnostics( # error if the test is not running on linux since we won't have the correct # deps installed when testing on MacOS. with patch( - "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices", - [BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45")], + "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices_and_advertisement_data", + { + "44:44:33:11:23:45": ( + BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45"), + generate_advertisement_data(local_name="x"), + ) + }, ), patch( "homeassistant.components.bluetooth.diagnostics.platform.system", return_value="Linux", @@ -68,15 +73,23 @@ async def test_diagnostics( "adapters": { "hci0": { "address": "00:00:00:00:00:01", - "hw_version": "usbid:1234", + "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, - "sw_version": "BlueZ 4.63", + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, "hci1": { "address": "00:00:00:00:00:02", - "hw_version": "usbid:1234", + "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": True, - "sw_version": "BlueZ 4.63", + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, }, "dbus": { @@ -99,15 +112,23 @@ async def test_diagnostics( "adapters": { "hci0": { "address": "00:00:00:00:00:01", - "hw_version": "usbid:1234", + "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, - "sw_version": "BlueZ 4.63", + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, "hci1": { "address": "00:00:00:00:00:02", - "hw_version": "usbid:1234", + "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": True, - "sw_version": "BlueZ 4.63", + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, }, "advertisement_tracker": { @@ -120,8 +141,22 @@ async def test_diagnostics( "scanners": [ { "adapter": "hci0", - "discovered_devices": [ - {"address": "44:44:33:11:23:45", "name": "x"} + "discovered_devices_and_advertisement_data": [ + { + "address": "44:44:33:11:23:45", + "advertisement_data": [ + "x", + {}, + {}, + [], + -127, + -127, + [[]], + ], + "name": "x", + "rssi": -60, + "details": None, + } ], "last_detection": ANY, "name": "hci0 (00:00:00:00:00:01)", @@ -131,8 +166,22 @@ async def test_diagnostics( }, { "adapter": "hci0", - "discovered_devices": [ - {"address": "44:44:33:11:23:45", "name": "x"} + "discovered_devices_and_advertisement_data": [ + { + "address": "44:44:33:11:23:45", + "advertisement_data": [ + "x", + {}, + {}, + [], + -127, + -127, + [[]], + ], + "name": "x", + "rssi": -60, + "details": None, + } ], "last_detection": ANY, "name": "hci0 (00:00:00:00:00:01)", @@ -142,8 +191,22 @@ async def test_diagnostics( }, { "adapter": "hci1", - "discovered_devices": [ - {"address": "44:44:33:11:23:45", "name": "x"} + "discovered_devices_and_advertisement_data": [ + { + "address": "44:44:33:11:23:45", + "advertisement_data": [ + "x", + {}, + {}, + [], + -127, + -127, + [[]], + ], + "name": "x", + "rssi": -60, + "details": None, + } ], "last_detection": ANY, "name": "hci1 (00:00:00:00:00:02)", @@ -171,8 +234,13 @@ async def test_diagnostics_macos( ) with patch( - "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices", - [BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45")], + "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices_and_advertisement_data", + { + "44:44:33:11:23:45": ( + BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45"), + switchbot_adv, + ) + }, ), patch( "homeassistant.components.bluetooth.diagnostics.platform.system", return_value="Darwin", @@ -200,6 +268,10 @@ async def test_diagnostics_macos( "address": "00:00:00:00:00:00", "passive_scan": False, "sw_version": ANY, + "manufacturer": "Apple", + "product": "Unknown MacOS Model", + "product_id": "Unknown", + "vendor_id": "Unknown", } }, "manager": { @@ -208,6 +280,10 @@ async def test_diagnostics_macos( "address": "00:00:00:00:00:00", "passive_scan": False, "sw_version": ANY, + "manufacturer": "Apple", + "product": "Unknown MacOS Model", + "product_id": "Unknown", + "vendor_id": "Unknown", } }, "advertisement_tracker": { @@ -227,6 +303,10 @@ async def test_diagnostics_macos( -127, [[]], ], + "device": { + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + }, "connectable": True, "manufacturer_data": { "1": {"__type": "", "repr": "b'\\x01'"} @@ -251,6 +331,10 @@ async def test_diagnostics_macos( -127, [[]], ], + "device": { + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + }, "connectable": True, "manufacturer_data": { "1": {"__type": "", "repr": "b'\\x01'"} @@ -266,8 +350,27 @@ async def test_diagnostics_macos( "scanners": [ { "adapter": "Core Bluetooth", - "discovered_devices": [ - {"address": "44:44:33:11:23:45", "name": "x"} + "discovered_devices_and_advertisement_data": [ + { + "address": "44:44:33:11:23:45", + "advertisement_data": [ + "wohand", + { + "1": { + "__type": "", + "repr": "b'\\x01'", + } + }, + {}, + [], + -127, + -127, + [[]], + ], + "name": "x", + "rssi": -60, + "details": None, + } ], "last_detection": ANY, "name": "Core Bluetooth", diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 5a5437af71a..9af1c0f313b 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -6,23 +6,23 @@ from unittest.mock import ANY, MagicMock, Mock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice +from bluetooth_adapters import DEFAULT_ADDRESS import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( + BaseHaScanner, BluetoothChange, BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, async_rediscover_address, async_track_unavailable, - models, scanner, ) from homeassistant.components.bluetooth.const import ( BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_PASSIVE, - DEFAULT_ADDRESS, DOMAIN, LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, @@ -36,6 +36,7 @@ from homeassistant.components.bluetooth.match import ( SERVICE_DATA_UUID, SERVICE_UUID, ) +from homeassistant.components.bluetooth.wrappers import HaBleakScannerWrapper from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback @@ -2210,7 +2211,7 @@ async def test_wrapped_instance_with_filter( empty_adv = generate_advertisement_data(local_name="empty") assert _get_manager() is not None - scanner = models.HaBleakScannerWrapper( + scanner = HaBleakScannerWrapper( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) @@ -2282,7 +2283,7 @@ async def test_wrapped_instance_with_service_uuids( empty_adv = generate_advertisement_data(local_name="empty") assert _get_manager() is not None - scanner = models.HaBleakScannerWrapper( + scanner = HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) @@ -2332,7 +2333,7 @@ async def test_wrapped_instance_with_broken_callbacks( ) assert _get_manager() is not None - scanner = models.HaBleakScannerWrapper( + scanner = HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) @@ -2381,7 +2382,7 @@ async def test_wrapped_instance_changes_uuids( empty_adv = generate_advertisement_data(local_name="empty") assert _get_manager() is not None - scanner = models.HaBleakScannerWrapper() + scanner = HaBleakScannerWrapper() scanner.set_scanning_filter( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) @@ -2436,7 +2437,7 @@ async def test_wrapped_instance_changes_filters( empty_adv = generate_advertisement_data(local_name="empty") assert _get_manager() is not None - scanner = models.HaBleakScannerWrapper() + scanner = HaBleakScannerWrapper() scanner.set_scanning_filter( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) @@ -2468,7 +2469,7 @@ async def test_wrapped_instance_unsupported_filter( hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert _get_manager() is not None - scanner = models.HaBleakScannerWrapper() + scanner = HaBleakScannerWrapper() scanner.set_scanning_filter( filters={ "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], @@ -2522,7 +2523,7 @@ async def test_async_ble_device_from_address( async def test_can_unsetup_bluetooth_single_adapter_macos( - hass, mock_bleak_scanner_start, enable_bluetooth, macos_adapter + hass, mock_bleak_scanner_start, macos_adapter ): """Test we can setup and unsetup bluetooth.""" entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) @@ -2605,12 +2606,13 @@ async def test_auto_detect_bluetooth_adapters_linux_multiple(hass, two_adapters) assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 -async def test_auto_detect_bluetooth_adapters_linux_none_found(hass, bluez_dbus_mock): +async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): """Test we auto detect bluetooth adapters on linux with no adapters found.""" with patch( - "bluetooth_adapters.get_bluetooth_adapter_details", return_value={} - ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + "bluetooth_adapters.systems.platform.system", return_value="Linux" + ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + {}, ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -2620,9 +2622,7 @@ async def test_auto_detect_bluetooth_adapters_linux_none_found(hass, bluez_dbus_ async def test_auto_detect_bluetooth_adapters_macos(hass): """Test we auto detect bluetooth adapters on macos.""" - with patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" - ): + with patch("bluetooth_adapters.systems.platform.system", return_value="Darwin"): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() assert not hass.config_entries.async_entries(bluetooth.DOMAIN) @@ -2632,7 +2632,7 @@ async def test_auto_detect_bluetooth_adapters_macos(hass): async def test_no_auto_detect_bluetooth_adapters_windows(hass): """Test we auto detect bluetooth adapters on windows.""" with patch( - "homeassistant.components.bluetooth.util.platform.system", + "bluetooth_adapters.systems.platform.system", return_value="Windows", ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) @@ -2644,12 +2644,12 @@ async def test_no_auto_detect_bluetooth_adapters_windows(hass): async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_bluetooth): """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) - assert isinstance(scanner, models.HaBleakScannerWrapper) + assert isinstance(scanner, HaBleakScannerWrapper) async def test_scanner_count_connectable(hass, enable_bluetooth): """Test getting the connectable scanner count.""" - scanner = models.BaseHaScanner(hass, "any") + scanner = BaseHaScanner(hass, "any", "any") cancel = bluetooth.async_register_scanner(hass, scanner, False) assert bluetooth.async_scanner_count(hass, connectable=True) == 1 cancel() @@ -2657,7 +2657,7 @@ async def test_scanner_count_connectable(hass, enable_bluetooth): async def test_scanner_count(hass, enable_bluetooth): """Test getting the connectable and non-connectable scanner count.""" - scanner = models.BaseHaScanner(hass, "any") + scanner = BaseHaScanner(hass, "any", "any") cancel = bluetooth.async_register_scanner(hass, scanner, False) assert bluetooth.async_scanner_count(hass, connectable=False) == 2 cancel() @@ -2710,23 +2710,21 @@ async def test_discover_new_usb_adapters(hass, mock_bleak_scanner_start, one_ada assert not hass.config_entries.flow.async_progress(DOMAIN) with patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={ + "bluetooth_adapters.systems.platform.system", return_value="Linux" + ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { "hci0": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:01", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - } + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", }, "hci1": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:02", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - } + "address": "00:00:00:00:00:02", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", }, }, ): @@ -2768,10 +2766,10 @@ async def test_discover_new_usb_adapters_with_firmware_fallback_delay( assert not hass.config_entries.flow.async_progress(DOMAIN) with patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={}, + "bluetooth_adapters.systems.platform.system", return_value="Linux" + ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + {}, ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) @@ -2781,23 +2779,21 @@ async def test_discover_new_usb_adapters_with_firmware_fallback_delay( assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 0 with patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={ + "bluetooth_adapters.systems.platform.system", return_value="Linux" + ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { "hci0": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:01", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - } + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", }, "hci1": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:02", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - } + "address": "00:00:00:00:00:02", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", }, }, ): diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 0375f68309f..e295291068a 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -1,14 +1,14 @@ """Tests for the Bluetooth integration manager.""" import time -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import patch from bleak.backends.scanner import BLEDevice from bluetooth_adapters import AdvertisementHistory import pytest from homeassistant.components import bluetooth -from homeassistant.components.bluetooth import models +from homeassistant.components.bluetooth import BaseHaScanner from homeassistant.components.bluetooth.manager import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) @@ -26,9 +26,8 @@ from . import ( @pytest.fixture def register_hci0_scanner(hass: HomeAssistant) -> None: """Register an hci0 scanner.""" - cancel = bluetooth.async_register_scanner( - hass, models.BaseHaScanner(hass, "hci0"), True - ) + hci0_scanner = BaseHaScanner(hass, "hci0", "hci0") + cancel = bluetooth.async_register_scanner(hass, hci0_scanner, True) yield cancel() @@ -36,9 +35,8 @@ def register_hci0_scanner(hass: HomeAssistant) -> None: @pytest.fixture def register_hci1_scanner(hass: HomeAssistant) -> None: """Register an hci1 scanner.""" - cancel = bluetooth.async_register_scanner( - hass, models.BaseHaScanner(hass, "hci1"), True - ) + hci1_scanner = BaseHaScanner(hass, "hci1", "hci1") + cancel = bluetooth.async_register_scanner(hass, hci1_scanner, True) yield cancel() @@ -275,8 +273,8 @@ async def test_restore_history_from_dbus(hass, one_adapter): } with patch( - "bluetooth_adapters.BlueZDBusObjects", - return_value=MagicMock(load=AsyncMock(), history=history), + "bluetooth_adapters.systems.linux.LinuxAdapters.history", + history, ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -420,7 +418,7 @@ async def test_switching_adapters_when_one_goes_away( ): """Test switching adapters when one goes away.""" cancel_hci2 = bluetooth.async_register_scanner( - hass, models.BaseHaScanner(hass, "hci2"), True + hass, BaseHaScanner(hass, "hci2", "hci2"), True ) address = "44:44:33:11:23:45" @@ -464,3 +462,55 @@ async def test_switching_adapters_when_one_goes_away( bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_poor_signal ) + + +async def test_switching_adapters_when_one_stop_scanning( + hass, enable_bluetooth, register_hci0_scanner +): + """Test switching adapters when stops scanning.""" + hci2_scanner = BaseHaScanner(hass, "hci2", "hci2") + cancel_hci2 = bluetooth.async_register_scanner(hass, hci2_scanner, True) + + address = "44:44:33:11:23:45" + + switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal") + switchbot_adv_good_signal = generate_advertisement_data( + local_name="wohand_good_signal", service_uuids=[], rssi=-60 + ) + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci2" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal") + switchbot_adv_poor_signal = generate_advertisement_data( + local_name="wohand_poor_signal", service_uuids=[], rssi=-100 + ) + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # We want to prefer the good signal when we have options + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + hci2_scanner.scanning = False + + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # Now that hci2 has stopped scanning, we should prefer the poor signal + # since poor signal is better than no signal + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal + ) + + cancel_hci2() diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index adb953b2af2..e200450f656 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -4,19 +4,19 @@ from __future__ import annotations from unittest.mock import patch import bleak -from bleak import BleakClient, BleakError +from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData import pytest -from homeassistant.components.bluetooth.models import ( - BaseHaScanner, +from homeassistant.components.bluetooth import BaseHaScanner, HaBluetoothConnector +from homeassistant.components.bluetooth.wrappers import ( HaBleakClientWrapper, HaBleakScannerWrapper, - HaBluetoothConnector, ) from . import ( + MockBleakClient, _get_manager, generate_advertisement_data, inject_advertisement, @@ -24,32 +24,6 @@ from . import ( ) -class MockBleakClient(BleakClient): - """Mock bleak client.""" - - def __init__(self, *args, **kwargs): - """Mock init.""" - super().__init__(*args, **kwargs) - self._device_path = "/dev/test" - - @property - def is_connected(self) -> bool: - """Mock connected.""" - return True - - async def connect(self, *args, **kwargs): - """Mock connect.""" - return True - - async def disconnect(self, *args, **kwargs): - """Mock disconnect.""" - pass - - async def get_services(self, *args, **kwargs): - """Mock get_services.""" - return [] - - async def test_wrapped_bleak_scanner(hass, enable_bluetooth): """Test wrapped bleak scanner dispatches calls as expected.""" scanner = HaBleakScannerWrapper() @@ -71,6 +45,7 @@ async def test_wrapped_bleak_client_raises_device_missing(hass, enable_bluetooth await client.connect() assert client.is_connected is False await client.disconnect() + assert await client.clear_cache() is False async def test_wrapped_bleak_client_set_disconnected_callback_before_connected( @@ -86,22 +61,73 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected( hass, enable_bluetooth, one_adapter ): """Test wrapped bleak client can set a disconnected callback after connected.""" + manager = _get_manager() + + switchbot_proxy_device_has_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: True + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-40, + ) + switchbot_proxy_device_adv_has_connection_slot = generate_advertisement_data( + local_name="wohand", + service_uuids=[], + manufacturer_data={1: b"\x01"}, + rssi=-40, + ) switchbot_device = BLEDevice( - "44:44:33:11:23:45", "wohand", {"path": "/org/bluez/hci0/dev_44_44_33_11_23_45"} + "44:44:33:11:23:45", + "wohand", + {"path": "/org/bluez/hci0/dev_44_44_33_11_23_45"}, ) switchbot_adv = generate_advertisement_data( - local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}, rssi=-100 ) - inject_advertisement(hass, switchbot_device, switchbot_adv) - client = HaBleakClientWrapper(switchbot_device) - with patch( - "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect" - ) as connect: + + inject_advertisement_with_source( + hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01" + ) + inject_advertisement_with_source( + hass, + switchbot_proxy_device_has_connection_slot, + switchbot_proxy_device_adv_has_connection_slot, + "esp32_has_connection_slot", + ) + + class FakeScanner(BaseHaScanner): + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices.""" + return { + switchbot_proxy_device_has_connection_slot.address: ( + switchbot_proxy_device_has_connection_slot, + switchbot_proxy_device_adv_has_connection_slot, + ) + } + + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Return a list of discovered devices.""" + if address == switchbot_proxy_device_has_connection_slot.address: + return switchbot_proxy_device_has_connection_slot + return None + + scanner = FakeScanner(hass, "esp32", "esp32") + cancel = manager.async_register_scanner(scanner, True) + + client = HaBleakClientWrapper(switchbot_proxy_device_has_connection_slot) + with patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect"): await client.connect() - assert len(connect.mock_calls) == 1 - assert client._backend is not None + assert client.is_connected is True client.set_disconnected_callback(lambda client: None) await client.disconnect() + cancel() async def test_ble_device_with_proxy_client_out_of_connections( @@ -143,6 +169,62 @@ async def test_ble_device_with_proxy_client_out_of_connections( await client.disconnect() +async def test_ble_device_with_proxy_clear_cache(hass, enable_bluetooth, one_adapter): + """Test we can clear cache on the proxy.""" + manager = _get_manager() + + switchbot_proxy_device_with_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: True + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-30, + ) + switchbot_adv = generate_advertisement_data( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) + + class FakeScanner(BaseHaScanner): + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices.""" + return { + switchbot_proxy_device_with_connection_slot.address: ( + switchbot_proxy_device_with_connection_slot, + switchbot_adv, + ) + } + + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Return a list of discovered devices.""" + if address == switchbot_proxy_device_with_connection_slot.address: + return switchbot_adv + return None + + scanner = FakeScanner(hass, "esp32", "esp32") + cancel = manager.async_register_scanner(scanner, True) + inject_advertisement_with_source( + hass, switchbot_proxy_device_with_connection_slot, switchbot_adv, "esp32" + ) + + assert manager.async_discovered_devices(True) == [ + switchbot_proxy_device_with_connection_slot + ] + + client = HaBleakClientWrapper(switchbot_proxy_device_with_connection_slot) + await client.connect() + assert client.is_connected is True + assert await client.clear_cache() is True + await client.disconnect() + cancel() + + async def test_ble_device_with_proxy_client_out_of_connections_uses_best_available( hass, enable_bluetooth, one_adapter ): @@ -226,7 +308,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab return switchbot_proxy_device_has_connection_slot return None - scanner = FakeScanner(hass, "esp32") + scanner = FakeScanner(hass, "esp32", "esp32") cancel = manager.async_register_scanner(scanner, True) assert manager.async_discovered_devices(True) == [ switchbot_proxy_device_no_connection_slot @@ -332,7 +414,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab return switchbot_proxy_device_has_connection_slot return None - scanner = FakeScanner(hass, "esp32") + scanner = FakeScanner(hass, "esp32", "esp32") cancel = manager.async_register_scanner(scanner, True) assert manager.async_discovered_devices(True) == [ switchbot_proxy_device_no_connection_slot diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 1bea3b149cd..dc6d88ca5d1 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -7,14 +7,14 @@ import bleak from bleak.backends.device import BLEDevice import bleak_retry_connector -from homeassistant.components.bluetooth.models import ( - HaBleakClientWrapper, - HaBleakScannerWrapper, -) from homeassistant.components.bluetooth.usage import ( install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher, ) +from homeassistant.components.bluetooth.wrappers import ( + HaBleakClientWrapper, + HaBleakScannerWrapper, +) from . import _get_manager @@ -33,7 +33,8 @@ async def test_multiple_bleak_scanner_instances(hass): uninstall_multiple_bleak_catcher() - instance = bleak.BleakScanner() + with patch("bleak.get_platform_scanner_backend_type"): + instance = bleak.BleakScanner() assert not isinstance(instance, HaBleakScannerWrapper) diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 585c83f20a7..04f8c534cf2 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -31,7 +31,6 @@ class MockBleakClient: def __init__(self, *args, **kwargs): """Mock BleakClient.""" - pass async def __aenter__(self, *args, **kwargs): """Mock BleakClient.__aenter__.""" @@ -39,7 +38,6 @@ class MockBleakClient: async def __aexit__(self, *args, **kwargs): """Mock BleakClient.__aexit__.""" - pass class MockBleakClientTimesOut(MockBleakClient): diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 58e684a1378..ee835493e66 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -21,6 +21,7 @@ from homeassistant.components.braviatv.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.core import HomeAssistant from homeassistant.helpers import instance_id from tests.common import MockConfigEntry @@ -381,7 +382,7 @@ async def test_create_entry_psk(hass): } -async def test_options_flow(hass): +async def test_options_flow(hass: HomeAssistant) -> None: """Test config flow options.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -422,6 +423,43 @@ async def test_options_flow(hass): assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} +async def test_options_flow_error(hass: HomeAssistant) -> None: + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="very_unique_string", + data={ + CONF_HOST: "bravia-host", + CONF_PIN: "1234", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + }, + title="TV-Model", + ) + config_entry.add_to_hass(hass) + + with patch("pybravia.BraviaTV.connect"), patch( + "pybravia.BraviaTV.get_power_status", + return_value="active", + ), patch( + "pybravia.BraviaTV.get_external_status", + return_value=BRAVIA_SOURCES, + ), patch( + "pybravia.BraviaTV.send_rest_req", + return_value={}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with patch( + "pybravia.BraviaTV.send_rest_req", + side_effect=BraviaTVError, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "failed_update" + + @pytest.mark.parametrize( "user_input", [{CONF_PIN: "mypsk", CONF_USE_PSK: True}, {CONF_PIN: "1234", CONF_USE_PSK: False}], diff --git a/tests/components/bsblan/fixtures/diagnostics.json b/tests/components/bsblan/fixtures/diagnostics.json new file mode 100644 index 00000000000..bd05aca56d5 --- /dev/null +++ b/tests/components/bsblan/fixtures/diagnostics.json @@ -0,0 +1,75 @@ +{ + "info": { + "device_identification": { + "name": "Gerte-Identifikation", + "unit": "", + "desc": "", + "value": "RVS21.831F/127", + "dataType": 7 + }, + "controller_family": { + "name": "Device family", + "unit": "", + "desc": "", + "value": "211", + "dataType": 0 + }, + "controller_variant": { + "name": "Device variant", + "unit": "", + "desc": "", + "value": "127", + "dataType": 0 + } + }, + "device": { + "name": "BSB-LAN", + "version": "1.0.38-20200730234859", + "MAC": "00:80:41:19:69:90", + "uptime": 969402857 + }, + "state": { + "hvac_mode": { + "name": "Operating mode", + "unit": "", + "desc": "Komfort", + "value": "heat", + "dataType": 1 + }, + "hvac_mode2": { + "name": "Operating mode", + "unit": "", + "desc": "Reduziert", + "value": "2", + "dataType": 1 + }, + "target_temperature": { + "name": "Room temperature Comfort setpoint", + "unit": "°C", + "desc": "", + "value": "18.5", + "dataType": 0 + }, + "hvac_action": { + "name": "Status heating circuit 1", + "unit": "", + "desc": "Raumtemp\u2019begrenzung", + "value": "122", + "dataType": 1 + }, + "current_temperature": { + "name": "Room temp 1 actual value", + "unit": "°C", + "desc": "", + "value": "18.6", + "dataType": 0 + }, + "room1_thermostat_mode": { + "name": "Raumthermostat 1", + "unit": "", + "desc": "Kein Bedarf", + "value": "0", + "dataType": 1 + } + } +} diff --git a/tests/components/bsblan/test_diagnostics.py b/tests/components/bsblan/test_diagnostics.py new file mode 100644 index 00000000000..6af6972e577 --- /dev/null +++ b/tests/components/bsblan/test_diagnostics.py @@ -0,0 +1,24 @@ +"""Tests for the diagnostics data provided by the BSBLan integration.""" +import json + +from aiohttp import ClientSession + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + + diagnostics_fixture = json.loads(load_fixture("bsblan/diagnostics.json")) + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == diagnostics_fixture + ) diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index 25ccb72edfa..2951413b0e6 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -85,7 +85,7 @@ NOT_BTHOME_SERVICE_INFO = BluetoothServiceInfoBleak( ) -def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBleak: +def make_bthome_v1_adv(address: str, payload: bytes) -> BluetoothServiceInfoBleak: """Make a dummy advertisement.""" return BluetoothServiceInfoBleak( name="Test Device", @@ -104,7 +104,7 @@ def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBlea ) -def make_encrypted_advertisement( +def make_encrypted_bthome_v1_adv( address: str, payload: bytes ) -> BluetoothServiceInfoBleak: """Make a dummy encrypted advertisement.""" @@ -123,3 +123,22 @@ def make_encrypted_advertisement( time=0, connectable=False, ) + + +def make_bthome_v2_adv(address: str, payload: bytes) -> BluetoothServiceInfoBleak: + """Make a dummy advertisement.""" + return BluetoothServiceInfoBleak( + name="Test Device", + address=address, + device=BLEDevice(address, None), + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fcd2-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000fcd2-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=generate_advertisement_data(local_name="Test Device"), + time=0, + connectable=False, + ) diff --git a/tests/components/bthome/test_binary_sensor.py b/tests/components/bthome/test_binary_sensor.py index 64b19b17a81..99c3b310678 100644 --- a/tests/components/bthome/test_binary_sensor.py +++ b/tests/components/bthome/test_binary_sensor.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.bthome.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON -from . import make_advertisement +from . import make_bthome_v1_adv, make_bthome_v2_adv from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) [ ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x10\x01", ), @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x11\x00", ), @@ -50,7 +50,7 @@ _LOGGER = logging.getLogger(__name__) ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x0F\x01", ), @@ -65,14 +65,100 @@ _LOGGER = logging.getLogger(__name__) ), ], ) -async def test_binary_sensors( +async def test_v1_binary_sensors( hass, mac_address, advertisement, bind_key, result, ): - """Test the different binary sensors.""" + """Test the different BTHome v1 binary sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac_address, + data={"bindkey": bind_key}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + advertisement, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == len(result) + for meas in result: + binary_sensor = hass.states.get(meas["binary_sensor_entity"]) + binary_sensor_attr = binary_sensor.attributes + assert binary_sensor.state == meas["expected_state"] + assert binary_sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "mac_address, advertisement, bind_key, result", + [ + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x10\x01", + ), + None, + [ + { + "binary_sensor_entity": "binary_sensor.test_device_18b2_power", + "friendly_name": "Test Device 18B2 Power", + "expected_state": STATE_ON, + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x11\x00", + ), + None, + [ + { + "binary_sensor_entity": "binary_sensor.test_device_18b2_opening", + "friendly_name": "Test Device 18B2 Opening", + "expected_state": STATE_OFF, + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0F\x01", + ), + None, + [ + { + "binary_sensor_entity": "binary_sensor.test_device_18b2_generic", + "friendly_name": "Test Device 18B2 Generic", + "expected_state": STATE_ON, + }, + ], + ), + ], +) +async def test_v2_binary_sensors( + hass, + mac_address, + advertisement, + bind_key, + result, +): + """Test the different BTHome v2 binary sensors.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=mac_address, diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 78b247aa393..dce82ab454f 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1,24 +1,29 @@ """Test the BTHome sensors.""" +import logging + import pytest from homeassistant.components.bthome.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT -from . import make_advertisement, make_encrypted_advertisement +from . import make_bthome_v1_adv, make_bthome_v2_adv, make_encrypted_bthome_v1_adv from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info +_LOGGER = logging.getLogger(__name__) + +# Tests for BTHome v1 @pytest.mark.parametrize( "mac_address, advertisement, bind_key, result", [ ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"#\x02\xca\t\x03\x03\xbf\x13", ), @@ -42,7 +47,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x00\xa8#\x02]\t\x03\x03\xb7\x18\x02\x01]", ), @@ -73,7 +78,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x00\x0c\x04\x04\x13\x8a\x01", ), @@ -90,7 +95,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "AA:BB:CC:DD:EE:FF", - make_advertisement( + make_bthome_v1_adv( "AA:BB:CC:DD:EE:FF", b"\x04\x05\x13\x8a\x14", ), @@ -107,7 +112,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x06\x5e\x1f", ), @@ -124,7 +129,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x07\x3e\x1d", ), @@ -141,7 +146,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x23\x08\xCA\x06", ), @@ -158,7 +163,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x09\x60", ), @@ -174,7 +179,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x04\n\x13\x8a\x14", ), @@ -191,7 +196,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x04\x0b\x02\x1b\x00", ), @@ -208,7 +213,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x0c\x02\x0c", ), @@ -225,7 +230,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\r\x12\x0c\x03\x0e\x02\x1c", ), @@ -249,7 +254,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x12\xe2\x04", ), @@ -266,7 +271,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x133\x01", ), @@ -283,7 +288,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x14\x02\x0c", ), @@ -300,7 +305,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ( "54:48:E6:8F:80:A5", - make_encrypted_advertisement( + make_encrypted_bthome_v1_adv( "54:48:E6:8F:80:A5", b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99', ), @@ -324,14 +329,14 @@ from tests.components.bluetooth import inject_bluetooth_service_info ), ], ) -async def test_sensors( +async def test_v1_sensors( hass, mac_address, advertisement, bind_key, result, ): - """Test the different measurement sensors.""" + """Test the different BTHome V1 sensors.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=mac_address, @@ -357,7 +362,572 @@ async def test_sensors( assert sensor.state == meas["expected_state"] assert sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] if ATTR_UNIT_OF_MEASUREMENT in sensor_attr: - # Count sensor does not have a unit of measurement + # Some sensors don't have a unit of measurement + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] + assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +# Tests for BTHome V2 +@pytest.mark.parametrize( + "mac_address, advertisement, bind_key, result", + [ + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x02\xca\x09\x03\xbf\x13", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity", + "friendly_name": "Test Device 18B2 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "50.55", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x01\x5d\x02\x5d\x09\x03\xb7\x18", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "23.97", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity", + "friendly_name": "Test Device 18B2 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "63.27", + }, + { + "sensor_entity": "sensor.test_device_18b2_battery", + "friendly_name": "Test Device 18B2 Battery", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "93", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x04\x13\x8a\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_pressure", + "friendly_name": "Test Device 18B2 Pressure", + "unit_of_measurement": "mbar", + "state_class": "measurement", + "expected_state": "1008.83", + }, + ], + ), + ( + "AA:BB:CC:DD:EE:FF", + make_bthome_v2_adv( + "AA:BB:CC:DD:EE:FF", + b"\x40\x05\x13\x8a\x14", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_eeff_illuminance", + "friendly_name": "Test Device EEFF Illuminance", + "unit_of_measurement": "lx", + "state_class": "measurement", + "expected_state": "13460.67", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x06\x5E\x1F", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_mass", + "friendly_name": "Test Device 18B2 Mass", + "unit_of_measurement": "kg", + "state_class": "measurement", + "expected_state": "80.3", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x07\x3E\x1d", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_mass", + "friendly_name": "Test Device 18B2 Mass", + "unit_of_measurement": "lb", + "state_class": "measurement", + "expected_state": "74.86", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x08\xCA\x06", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_dew_point", + "friendly_name": "Test Device 18B2 Dew Point", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "17.38", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x09\x60", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_count", + "friendly_name": "Test Device 18B2 Count", + "state_class": "measurement", + "expected_state": "96", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0a\x13\x8a\x14", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_energy", + "friendly_name": "Test Device 18B2 Energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + "expected_state": "1346.067", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0b\x02\x1b\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_power", + "friendly_name": "Test Device 18B2 Power", + "unit_of_measurement": "W", + "state_class": "measurement", + "expected_state": "69.14", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0c\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_voltage", + "friendly_name": "Test Device 18B2 Voltage", + "unit_of_measurement": "V", + "state_class": "measurement", + "expected_state": "3.074", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0d\x12\x0c\x0e\x02\x1c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_pm10", + "friendly_name": "Test Device 18B2 Pm10", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "7170", + }, + { + "sensor_entity": "sensor.test_device_18b2_pm25", + "friendly_name": "Test Device 18B2 Pm25", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "3090", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x12\xe2\x04", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_carbon_dioxide", + "friendly_name": "Test Device 18B2 Carbon Dioxide", + "unit_of_measurement": "ppm", + "state_class": "measurement", + "expected_state": "1250", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x133\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_volatile_organic_compounds", + "friendly_name": "Test Device 18B2 Volatile Organic Compounds", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "307", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x14\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_moisture", + "friendly_name": "Test Device 18B2 Moisture", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "30.74", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x3F\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_rotation", + "friendly_name": "Test Device 18B2 Rotation", + "unit_of_measurement": "°", + "state_class": "measurement", + "expected_state": "307.4", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x40\x0C\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_distance", + "friendly_name": "Test Device 18B2 Distance", + "unit_of_measurement": "mm", + "state_class": "measurement", + "expected_state": "12", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x41\x4E\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_distance", + "friendly_name": "Test Device 18B2 Distance", + "unit_of_measurement": "m", + "state_class": "measurement", + "expected_state": "7.8", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x42\x4E\x34\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_duration", + "friendly_name": "Test Device 18B2 Duration", + "unit_of_measurement": "s", + "state_class": "measurement", + "expected_state": "13.39", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x43\x4E\x34", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_current", + "friendly_name": "Test Device 18B2 Current", + "unit_of_measurement": "A", + "state_class": "measurement", + "expected_state": "13.39", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x44\x4E\x34", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_speed", + "friendly_name": "Test Device 18B2 Speed", + "unit_of_measurement": "m/s", + "state_class": "measurement", + "expected_state": "133.9", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x45\x11\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "27.3", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x46\x32", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_uv_index", + "friendly_name": "Test Device 18B2 Uv Index", + "state_class": "measurement", + "expected_state": "5.0", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x02\xca\x09\x02\xcf\x09", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature_1", + "friendly_name": "Test Device 18B2 Temperature 1", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_18b2_temperature_2", + "friendly_name": "Test Device 18B2 Temperature 2", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.11", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x02\xca\x09\x02\xcf\x09\x02\xcf\x08\x03\xb7\x18\x03\xb7\x17\x01\x5d", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature_1", + "friendly_name": "Test Device 18B2 Temperature 1", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_18b2_temperature_2", + "friendly_name": "Test Device 18B2 Temperature 2", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.11", + }, + { + "sensor_entity": "sensor.test_device_18b2_temperature_3", + "friendly_name": "Test Device 18B2 Temperature 3", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "22.55", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity_1", + "friendly_name": "Test Device 18B2 Humidity 1", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "63.27", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity_2", + "friendly_name": "Test Device 18B2 Humidity 2", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "60.71", + }, + { + "sensor_entity": "sensor.test_device_18b2_battery", + "friendly_name": "Test Device 18B2 Battery", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "93", + }, + ], + ), + ( + "54:48:E6:8F:80:A5", + make_bthome_v2_adv( + "54:48:E6:8F:80:A5", + b"\x41\xa4\x72\x66\xc9\x5f\x73\x00\x11\x22\x33\x78\x23\x72\x14", + ), + "231d39c1d7cc1ab1aee224cd096db932", + [ + { + "sensor_entity": "sensor.test_device_80a5_temperature", + "friendly_name": "Test Device 80A5 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_80a5_humidity", + "friendly_name": "Test Device 80A5 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "50.55", + }, + ], + ), + ], +) +async def test_v2_sensors( + hass, + mac_address, + advertisement, + bind_key, + result, +): + """Test the different BTHome V2 sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac_address, + data={"bindkey": bind_key}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + advertisement, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == len(result) + + for meas in result: + _LOGGER.error(meas) + sensor = hass.states.get(meas["sensor_entity"]) + _LOGGER.error(hass.states) + sensor_attr = sensor.attributes + assert sensor.state == meas["expected_state"] + assert sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] + if ATTR_UNIT_OF_MEASUREMENT in sensor_attr: + # Some sensors don't have a unit of measurement assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index f35fa609e4a..b936a02db87 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -939,5 +939,8 @@ async def test_get_events_custom_calendars(hass, calendar, get_api_events): "summary": "This is a normal event", "location": "Hamburg", "description": "Surprisingly rainy", + "uid": None, + "recurrence_id": None, + "rrule": None, } ] diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 71415284d35..c6d3e1414b2 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -12,7 +12,6 @@ from homeassistant.components.camera.const import ( PREF_ORIENTATION, PREF_PRELOAD_STREAM, ) -from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( @@ -302,8 +301,9 @@ async def test_websocket_update_preload_prefs(hass, hass_ws_client, mock_camera) ) msg = await client.receive_json() - # There should be no preferences - assert not msg["result"] + # The default prefs should be returned. Preload stream should be False + assert msg["success"] + assert msg["result"][PREF_PRELOAD_STREAM] is False # Update the preference await client.send_json( @@ -367,7 +367,7 @@ async def test_websocket_update_orientation_prefs(hass, hass_ws_client, mock_cam assert response["success"] er_camera_prefs = registry.async_get("camera.demo_uniquecamera").options[DOMAIN] - assert er_camera_prefs[PREF_ORIENTATION] == 3 + assert er_camera_prefs[PREF_ORIENTATION] == camera.Orientation.ROTATE_180 assert response["result"][PREF_ORIENTATION] == er_camera_prefs[PREF_ORIENTATION] # Check that the preference was saved await client.send_json( @@ -375,7 +375,7 @@ async def test_websocket_update_orientation_prefs(hass, hass_ws_client, mock_cam ) msg = await client.receive_json() # orientation entry for this camera should have been added - assert msg["result"]["orientation"] == 3 + assert msg["result"]["orientation"] == camera.Orientation.ROTATE_180 async def test_play_stream_service_no_source(hass, mock_camera, mock_stream): @@ -421,12 +421,12 @@ async def test_handle_play_stream_service(hass, mock_camera, mock_stream): async def test_no_preload_stream(hass, mock_stream): """Test camera preload preference.""" - demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: False}) + demo_settings = camera.DynamicStreamSettings() with patch( "homeassistant.components.camera.Stream.endpoint_url", ) as mock_request_stream, patch( - "homeassistant.components.camera.prefs.CameraPreferences.get", - return_value=demo_prefs, + "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", + return_value=demo_settings, ), patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", new_callable=PropertyMock, @@ -440,12 +440,12 @@ async def test_no_preload_stream(hass, mock_stream): async def test_preload_stream(hass, mock_stream): """Test camera preload preference.""" - demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: True}) + demo_settings = camera.DynamicStreamSettings(preload_stream=True) with patch( "homeassistant.components.camera.create_stream" ) as mock_create_stream, patch( - "homeassistant.components.camera.prefs.CameraPreferences.get", - return_value=demo_prefs, + "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", + return_value=demo_settings, ), patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="http://example.com", @@ -503,15 +503,17 @@ async def test_camera_proxy_stream(hass, mock_camera, hass_client): client = await hass_client() - response = await client.get("/api/camera_proxy_stream/camera.demo_camera") - assert response.status == HTTPStatus.OK + async with client.get("/api/camera_proxy_stream/camera.demo_camera") as response: + assert response.status == HTTPStatus.OK with patch( "homeassistant.components.demo.camera.DemoCamera.handle_async_mjpeg_stream", return_value=None, ): - response = await client.get("/api/camera_proxy_stream/camera.demo_camera") - assert response.status == HTTPStatus.BAD_GATEWAY + async with await client.get( + "/api/camera_proxy_stream/camera.demo_camera" + ) as response: + assert response.status == HTTPStatus.BAD_GATEWAY async def test_websocket_web_rtc_offer( diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 4bd5868db5f..e16fb63b34a 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -52,6 +52,15 @@ def mock_cloud_login(hass, mock_cloud_setup): yield +@pytest.fixture(name="mock_auth") +def mock_auth_fixture(): + """Mock check token.""" + with patch("hass_nabucasa.auth.CognitoAuth.async_check_token"), patch( + "hass_nabucasa.auth.CognitoAuth.async_renew_access_token" + ): + yield + + @pytest.fixture def mock_expired_cloud_login(hass, mock_cloud_setup): """Mock cloud is logged in.""" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 4e0df3c8ee3..b7637780b12 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -129,7 +129,7 @@ async def test_alexa_config_report_state(hass, cloud_prefs, cloud_stub): async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): """Test Alexa config should expose using prefs.""" aioclient_mock.post( - "http://example/alexa_token", + "https://example/access_token", json={ "access_token": "mock-token", "event_endpoint": "http://example.com/alexa_endpoint", @@ -142,7 +142,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): "mock-user-id", cloud_prefs, Mock( - alexa_access_token_url="http://example/alexa_token", + alexa_server="example", auth=Mock(async_check_token=AsyncMock()), websession=async_get_clientsession(hass), ), @@ -181,7 +181,7 @@ async def test_alexa_config_fail_refresh_token( """Test Alexa config failing to refresh token.""" aioclient_mock.post( - "http://example/alexa_token", + "https://example/access_token", json={ "access_token": "mock-token", "event_endpoint": "http://example.com/alexa_endpoint", @@ -198,7 +198,7 @@ async def test_alexa_config_fail_refresh_token( "mock-user-id", cloud_prefs, Mock( - alexa_access_token_url="http://example/alexa_token", + alexa_server="example", auth=Mock(async_check_token=AsyncMock()), websession=async_get_clientsession(hass), ), @@ -228,7 +228,7 @@ async def test_alexa_config_fail_refresh_token( conf.async_invalidate_access_token() aioclient_mock.clear_requests() aioclient_mock.post( - "http://example/alexa_token", + "https://example/access_token", json={"reason": reject_reason}, status=400, ) @@ -254,7 +254,7 @@ async def test_alexa_config_fail_refresh_token( # State reporting should now be re-enabled for Alexa aioclient_mock.clear_requests() aioclient_mock.post( - "http://example/alexa_token", + "https://example/access_token", json={ "access_token": "mock-token", "event_endpoint": "http://example.com/alexa_endpoint", diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 4d0729d72b2..0dbc20d4f91 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -21,14 +21,7 @@ from . import mock_cloud, mock_cloud_prefs from tests.components.google_assistant import MockConfig -SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" - - -@pytest.fixture(name="mock_auth") -def mock_auth_fixture(): - """Mock check token.""" - with patch("hass_nabucasa.auth.CognitoAuth.async_check_token"): - yield +SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/payments/subscription_info" @pytest.fixture(name="mock_cloud_login") @@ -55,8 +48,8 @@ def setup_api_fixture(hass, aioclient_mock): "cognito_client_id": "cognito_client_id", "user_pool_id": "user_pool_id", "region": "region", - "relayer": "relayer", - "subscription_info_url": SUBSCRIPTION_INFO_URL, + "relayer_server": "relayer", + "accounts_server": "api-test.hass.io", "google_actions": {"filter": {"include_domains": "light"}}, "alexa": { "filter": {"include_entities": ["light.kitchen", "switch.ac"]} diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 78a8f83eef6..48caf27dfe2 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -26,13 +26,13 @@ async def test_constructor_loads_info_from_config(hass): "cognito_client_id": "test-cognito_client_id", "user_pool_id": "test-user_pool_id", "region": "test-region", - "relayer": "test-relayer", - "subscription_info_url": "http://test-subscription-info-url", - "cloudhook_create_url": "http://test-cloudhook_create_url", - "remote_api_url": "http://test-remote_api_url", - "alexa_access_token_url": "http://test-alexa-token-url", - "acme_directory_server": "http://test-acme-directory-server", - "google_actions_report_state_url": "http://test-google-actions-report-state-url", + "relayer_server": "test-relayer-server", + "accounts_server": "test-acounts-server", + "cloudhook_server": "test-cloudhook-server", + "remote_sni_server": "test-remote-sni-server", + "alexa_server": "test-alexa-server", + "acme_server": "test-acme-server", + "remotestate_server": "test-remotestate-server", }, }, ) @@ -43,16 +43,13 @@ async def test_constructor_loads_info_from_config(hass): assert cl.cognito_client_id == "test-cognito_client_id" assert cl.user_pool_id == "test-user_pool_id" assert cl.region == "test-region" - assert cl.relayer == "test-relayer" - assert cl.subscription_info_url == "http://test-subscription-info-url" - assert cl.cloudhook_create_url == "http://test-cloudhook_create_url" - assert cl.remote_api_url == "http://test-remote_api_url" - assert cl.alexa_access_token_url == "http://test-alexa-token-url" - assert cl.acme_directory_server == "http://test-acme-directory-server" - assert ( - cl.google_actions_report_state_url - == "http://test-google-actions-report-state-url" - ) + assert cl.relayer_server == "test-relayer-server" + assert cl.iot.ws_server_url == "wss://test-relayer-server/websocket" + assert cl.accounts_server == "test-acounts-server" + assert cl.cloudhook_server == "test-cloudhook-server" + assert cl.alexa_server == "test-alexa-server" + assert cl.acme_server == "test-acme-server" + assert cl.remotestate_server == "test-remotestate-server" async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): @@ -120,7 +117,7 @@ async def test_setup_existing_cloud_user(hass, hass_storage): "cognito_client_id": "test-cognito_client_id", "user_pool_id": "test-user_pool_id", "region": "test-region", - "relayer": "test-relayer", + "relayer_server": "test-relayer-serer", }, }, ) diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py new file mode 100644 index 00000000000..052cdde0d0d --- /dev/null +++ b/tests/components/cloud/test_repairs.py @@ -0,0 +1,232 @@ +"""Test cloud repairs.""" +from collections.abc import Awaitable, Callable, Generator +from datetime import timedelta +from http import HTTPStatus +from unittest.mock import AsyncMock, patch + +from aiohttp import ClientSession + +from homeassistant.components.cloud import DOMAIN +import homeassistant.components.cloud.repairs as cloud_repairs +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.core import HomeAssistant +import homeassistant.helpers.issue_registry as ir +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from . import mock_cloud + +from tests.common import async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_do_not_create_repair_issues_at_startup_if_not_logged_in( + hass: HomeAssistant, +): + """Test that we create repair issue at startup if we are logged in.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + + with patch("homeassistant.components.cloud.Cloud.is_logged_in", False): + await mock_cloud(hass) + + async_fire_time_changed(hass, dt.utcnow() + timedelta(hours=1)) + await hass.async_block_till_done() + + assert not issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) + + +async def test_create_repair_issues_at_startup_if_logged_in( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_auth: Generator[None, AsyncMock, None], +): + """Test that we create repair issue at startup if we are logged in.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + aioclient_mock.get( + "https://accounts.nabucasa.com/payments/subscription_info", + json={"provider": "legacy"}, + ) + + with patch("homeassistant.components.cloud.Cloud.is_logged_in", True): + await mock_cloud(hass) + + async_fire_time_changed(hass, dt.utcnow() + timedelta(hours=1)) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) + + +async def test_legacy_subscription_delete_issue_if_no_longer_legacy( + hass: HomeAssistant, +): + """Test that we delete the legacy subscription issue if no longer legacy.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + assert issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) + + cloud_repairs.async_manage_legacy_subscription_issue(hass, {}) + assert not issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) + + +async def test_legacy_subscription_repair_flow( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_auth: Generator[None, AsyncMock, None], + hass_client: Callable[..., Awaitable[ClientSession]], +): + """Test desired flow of the fix flow for legacy subscription.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + aioclient_mock.get( + "https://accounts.nabucasa.com/payments/subscription_info", + json={"provider": None}, + ) + + cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + repair_issue = issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) + assert repair_issue + + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + await mock_cloud(hass) + await hass.async_block_till_done() + await hass.async_start() + + client = await hass_client() + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": DOMAIN, "issue_id": repair_issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": DOMAIN, + "step_id": "confirm_change_plan", + "data_schema": [], + "errors": None, + "description_placeholders": None, + "last_step": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "external", + "flow_id": flow_id, + "handler": DOMAIN, + "step_id": "change_plan", + "url": "https://account.nabucasa.com/", + "description_placeholders": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "version": 1, + "type": "create_entry", + "flow_id": flow_id, + "handler": DOMAIN, + "title": "", + "description": None, + "description_placeholders": None, + } + + assert not issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) + + +async def test_legacy_subscription_repair_flow_timeout( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], +): + """Test timeout flow of the fix flow for legacy subscription.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + + cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) + repair_issue = issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) + assert repair_issue + + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + await mock_cloud(hass) + await hass.async_block_till_done() + await hass.async_start() + + client = await hass_client() + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": DOMAIN, "issue_id": repair_issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": DOMAIN, + "step_id": "confirm_change_plan", + "data_schema": [], + "errors": None, + "description_placeholders": None, + "last_step": None, + } + + with patch("homeassistant.components.cloud.repairs.MAX_RETRIES", new=0): + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "external", + "flow_id": flow_id, + "handler": DOMAIN, + "step_id": "change_plan", + "url": "https://account.nabucasa.com/", + "description_placeholders": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "abort", + "flow_id": flow_id, + "handler": "cloud", + "reason": "operation_took_too_long", + "description_placeholders": None, + "result": None, + } + + assert issue_registry.async_get_issue( + domain="cloud", issue_id="legacy_subscription" + ) diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index cc37788bc4c..dae5ac4b4cc 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -13,7 +13,7 @@ from tests.common import get_system_health_info async def test_cloud_system_health(hass, aioclient_mock): """Test cloud system health.""" aioclient_mock.get("https://cloud.bla.com/status", text="") - aioclient_mock.get("https://cert-server", text="") + aioclient_mock.get("https://cert-server/directory", text="") aioclient_mock.get( "https://cognito-idp.us-east-1.amazonaws.com/AAAA/.well-known/jwks.json", exc=ClientError, @@ -25,8 +25,8 @@ async def test_cloud_system_health(hass, aioclient_mock): hass.data["cloud"] = Mock( region="us-east-1", user_pool_id="AAAA", - relayer="wss://cloud.bla.com/websocket_api", - acme_directory_server="https://cert-server", + relayer_server="cloud.bla.com", + acme_server="cert-server", is_logged_in=True, remote=Mock(is_connected=False, snitun_server="us-west-1"), expiration_date=now, diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index ab7dbdab78e..6e7f450d711 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -1,11 +1,16 @@ """Test the Cloudflare integration.""" +from unittest.mock import patch + from pycfdns.exceptions import ( CloudflareAuthenticationException, CloudflareConnectionException, ) +import pytest from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.location import LocationInfo from . import ENTRY_CONFIG, init_integration @@ -70,12 +75,51 @@ async def test_integration_services(hass, cfupdate): entry = await init_integration(hass) assert entry.state is ConfigEntryState.LOADED - await hass.services.async_call( - DOMAIN, - SERVICE_UPDATE_RECORDS, - {}, - blocking=True, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.cloudflare.async_detect_location_info", + return_value=LocationInfo( + "0.0.0.0", + "US", + "USD", + "CA", + "California", + "San Diego", + "92122", + "America/Los_Angeles", + 32.8594, + -117.2073, + True, + ), + ): + + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_RECORDS, + {}, + blocking=True, + ) + await hass.async_block_till_done() instance.update_records.assert_called_once() + + +async def test_integration_services_with_issue(hass, cfupdate): + """Test integration services with issue.""" + instance = cfupdate.return_value + + entry = await init_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + with patch( + "homeassistant.components.cloudflare.async_detect_location_info", + return_value=None, + ), pytest.raises(HomeAssistantError, match="Could not get external IPv4 address"): + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_RECORDS, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + instance.update_records.assert_not_called() diff --git a/tests/components/config/conftest.py b/tests/components/config/conftest.py new file mode 100644 index 00000000000..e6f1532428e --- /dev/null +++ b/tests/components/config/conftest.py @@ -0,0 +1,73 @@ +"""Test fixtures for the config integration.""" +from contextlib import contextmanager +from copy import deepcopy +import json +import logging +from os.path import basename +from unittest.mock import patch + +import pytest + +from homeassistant.core import HomeAssistant + +from tests.common import raise_contains_mocks + +_LOGGER = logging.getLogger(__name__) + + +@contextmanager +def mock_config_store(data=None): + """Mock config yaml store. + + Data is a dict {'key': {'version': version, 'data': data}} + + Written data will be converted to JSON to ensure JSON parsing works. + """ + if data is None: + data = {} + + def mock_read(path): + """Mock version of load.""" + file_name = basename(path) + _LOGGER.info("Reading data from %s: %s", file_name, data.get(file_name)) + return deepcopy(data.get(file_name)) + + def mock_write(path, data_to_write): + """Mock version of write.""" + file_name = basename(path) + _LOGGER.info("Writing data to %s: %s", file_name, data_to_write) + raise_contains_mocks(data_to_write) + # To ensure that the data can be serialized + data[file_name] = json.loads(json.dumps(data_to_write)) + + async def mock_async_hass_config_yaml(hass: HomeAssistant) -> dict: + """Mock version of async_hass_config_yaml.""" + result = {} + # Return a configuration.yaml with "automation" mapped to the contents of + # automations.yaml and so on. + for key, value in data.items(): + result[key.partition(".")[0][0:-1]] = deepcopy(value) + _LOGGER.info("Reading data from configuration.yaml: %s", result) + return result + + with patch( + "homeassistant.components.config._read", + side_effect=mock_read, + autospec=True, + ), patch( + "homeassistant.components.config._write", + side_effect=mock_write, + autospec=True, + ), patch( + "homeassistant.config.async_hass_config_yaml", + side_effect=mock_async_hass_config_yaml, + autospec=True, + ): + yield data + + +@pytest.fixture +def hass_config_store(): + """Fixture to mock config yaml store.""" + with mock_config_store() as stored_data: + yield stored_data diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 6f782fdbbff..0a5a79c7d15 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -7,6 +7,7 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -23,19 +24,18 @@ async def setup_automation( @pytest.mark.parametrize("automation_config", ({},)) -async def test_get_device_config(hass, hass_client, setup_automation): - """Test getting device config.""" +async def test_get_automation_config( + hass: HomeAssistant, hass_client, hass_config_store, setup_automation +): + """Test getting automation config.""" with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) client = await hass_client() - def mock_read(path): - """Mock reading data.""" - return [{"id": "sun"}, {"id": "moon"}] + hass_config_store["automations.yaml"] = [{"id": "sun"}, {"id": "moon"}] - with patch("homeassistant.components.config._read", mock_read): - resp = await client.get("/api/config/automation/config/moon") + resp = await client.get("/api/config/automation/config/moon") assert resp.status == HTTPStatus.OK result = await resp.json() @@ -44,85 +44,81 @@ async def test_get_device_config(hass, hass_client, setup_automation): @pytest.mark.parametrize("automation_config", ({},)) -async def test_update_device_config(hass, hass_client, setup_automation): - """Test updating device config.""" +async def test_update_automation_config( + hass: HomeAssistant, hass_client, hass_config_store, setup_automation +): + """Test updating automation config.""" with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("automation")) == [] + client = await hass_client() orig_data = [{"id": "sun"}, {"id": "moon"}] + hass_config_store["automations.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data - - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.post( - "/api/config/automation/config/moon", - data=json.dumps({"trigger": [], "action": [], "condition": []}), - ) + resp = await client.post( + "/api/config/automation/config/moon", + data=json.dumps({"trigger": [], "action": [], "condition": []}), + ) + await hass.async_block_till_done() + assert sorted(hass.states.async_entity_ids("automation")) == [ + "automation.automation_0" + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - assert list(orig_data[1]) == ["id", "trigger", "condition", "action"] - assert orig_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} - assert written[0] == orig_data + new_data = hass_config_store["automations.yaml"] + assert list(new_data[1]) == ["id", "trigger", "condition", "action"] + assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} @pytest.mark.parametrize("automation_config", ({},)) -async def test_update_remove_key_device_config(hass, hass_client, setup_automation): - """Test updating device config while removing a key.""" +async def test_update_remove_key_automation_config( + hass: HomeAssistant, hass_client, hass_config_store, setup_automation +): + """Test updating automation config while removing a key.""" with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("automation")) == [] + client = await hass_client() orig_data = [{"id": "sun", "key": "value"}, {"id": "moon", "key": "value"}] + hass_config_store["automations.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data - - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.post( - "/api/config/automation/config/moon", - data=json.dumps({"trigger": [], "action": [], "condition": []}), - ) + resp = await client.post( + "/api/config/automation/config/moon", + data=json.dumps({"trigger": [], "action": [], "condition": []}), + ) + await hass.async_block_till_done() + assert sorted(hass.states.async_entity_ids("automation")) == [ + "automation.automation_0" + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - assert list(orig_data[1]) == ["id", "trigger", "condition", "action"] - assert orig_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} - assert written[0] == orig_data + new_data = hass_config_store["automations.yaml"] + assert list(new_data[1]) == ["id", "trigger", "condition", "action"] + assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} @pytest.mark.parametrize("automation_config", ({},)) -async def test_bad_formatted_automations(hass, hass_client, setup_automation): +async def test_bad_formatted_automations( + hass: HomeAssistant, hass_client, hass_config_store, setup_automation +): """Test that we handle automations without ID.""" with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("automation")) == [] + client = await hass_client() orig_data = [ @@ -132,34 +128,25 @@ async def test_bad_formatted_automations(hass, hass_client, setup_automation): }, {"id": "moon"}, ] + hass_config_store["automations.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data - - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.post( - "/api/config/automation/config/moon", - data=json.dumps({"trigger": [], "action": [], "condition": []}), - ) - await hass.async_block_till_done() + resp = await client.post( + "/api/config/automation/config/moon", + data=json.dumps({"trigger": [], "action": [], "condition": []}), + ) + await hass.async_block_till_done() + assert sorted(hass.states.async_entity_ids("automation")) == [ + "automation.automation_0" + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - # Verify ID added to orig_data - assert "id" in orig_data[0] - - assert orig_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} + # Verify ID added + new_data = hass_config_store["automations.yaml"] + assert "id" in new_data[0] + assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} @pytest.mark.parametrize( @@ -179,7 +166,9 @@ async def test_bad_formatted_automations(hass, hass_client, setup_automation): ], ), ) -async def test_delete_automation(hass, hass_client, setup_automation): +async def test_delete_automation( + hass: HomeAssistant, hass_client, hass_config_store, setup_automation +): """Test deleting an automation.""" ent_reg = er.async_get(hass) @@ -188,31 +177,27 @@ async def test_delete_automation(hass, hass_client, setup_automation): with patch.object(config, "SECTIONS", ["automation"]): assert await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("automation")) == [ + "automation.automation_0", + "automation.automation_1", + ] + client = await hass_client() orig_data = [{"id": "sun"}, {"id": "moon"}] + hass_config_store["automations.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data + resp = await client.delete("/api/config/automation/config/sun") + await hass.async_block_till_done() - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.delete("/api/config/automation/config/sun") - await hass.async_block_till_done() + assert sorted(hass.states.async_entity_ids("automation")) == [ + "automation.automation_1", + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - assert len(written) == 1 - assert written[0][0]["id"] == "moon" + assert hass_config_store["automations.yaml"] == [{"id": "moon"}] assert len(ent_reg.entities) == 1 diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 29a2395a926..8ebf59323c8 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -69,7 +69,6 @@ async def test_get_entries(hass, client, clear_handlers): @callback def async_get_options_flow(config_entry): """Get options flow.""" - pass @classmethod @callback diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 33309f6b6c6..67977c96a2b 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -60,6 +60,8 @@ async def test_websocket_core_update(hass, client): assert hass.config.external_url != "https://www.example.com" assert hass.config.internal_url != "http://example.com" assert hass.config.currency == "EUR" + assert hass.config.country != "SE" + assert hass.config.language != "sv" with patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz: await client.send_json( @@ -75,6 +77,8 @@ async def test_websocket_core_update(hass, client): "external_url": "https://www.example.com", "internal_url": "http://example.local", "currency": "USD", + "country": "SE", + "language": "sv", } ) diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 2d5610cadfb..9a54c55ff9e 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -2,7 +2,7 @@ from homeassistant.setup import async_setup_component -async def test_config_setup(hass, loop): +async def test_config_setup(hass, event_loop): """Test it sets up hassbian.""" await async_setup_component(hass, "config", {}) assert "config" in hass.config.components diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 69f75cc5895..781e4000c25 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,14 +1,13 @@ """Test Automation config panel.""" from http import HTTPStatus import json -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.helpers import entity_registry as er -from homeassistant.util.yaml import dump @pytest.fixture @@ -18,114 +17,98 @@ async def setup_scene(hass, scene_config): @pytest.mark.parametrize("scene_config", ({},)) -async def test_create_scene(hass, hass_client, setup_scene): +async def test_create_scene(hass, hass_client, hass_config_store, setup_scene): """Test creating a scene.""" with patch.object(config, "SECTIONS", ["scene"]): await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("scene")) == [] + client = await hass_client() - def mock_read(path): - """Mock reading data.""" - return None + orig_data = {} + hass_config_store["scenes.yaml"] = orig_data - written = [] + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + # "id": "light_off", # The id should be added when writing + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + await hass.async_block_till_done() - def mock_write(path, data): - """Mock writing data.""" - data = dump(data) - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.post( - "/api/config/scene/config/light_off", - data=json.dumps( - { - # "id": "light_off", - "name": "Lights off", - "entities": {"light.bedroom": {"state": "off"}}, - } - ), - ) + assert sorted(hass.states.async_entity_ids("scene")) == [ + "scene.lights_off", + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - assert len(written) == 1 - written_yaml = written[0] - assert ( - written_yaml - == """- id: light_off - name: Lights off - entities: - light.bedroom: - state: 'off' -""" - ) + assert hass_config_store["scenes.yaml"] == [ + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ] @pytest.mark.parametrize("scene_config", ({},)) -async def test_update_scene(hass, hass_client, setup_scene): +async def test_update_scene(hass, hass_client, hass_config_store, setup_scene): """Test updating a scene.""" with patch.object(config, "SECTIONS", ["scene"]): await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("scene")) == [] + client = await hass_client() orig_data = [{"id": "light_on"}, {"id": "light_off"}] + hass_config_store["scenes.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + await hass.async_block_till_done() - written = [] - - def mock_write(path, data): - """Mock writing data.""" - data = dump(data) - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.post( - "/api/config/scene/config/light_off", - data=json.dumps( - { - "id": "light_off", - "name": "Lights off", - "entities": {"light.bedroom": {"state": "off"}}, - } - ), - ) + assert sorted(hass.states.async_entity_ids("scene")) == [ + "scene.lights_off", + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - assert len(written) == 1 - written_yaml = written[0] - assert ( - written_yaml - == """- id: light_on -- id: light_off - name: Lights off - entities: - light.bedroom: - state: 'off' -""" - ) + assert hass_config_store["scenes.yaml"] == [ + {"id": "light_on"}, + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + }, + ] @pytest.mark.parametrize("scene_config", ({},)) -async def test_bad_formatted_scene(hass, hass_client, setup_scene): +async def test_bad_formatted_scene(hass, hass_client, hass_config_store, setup_scene): """Test that we handle scene without ID.""" with patch.object(config, "SECTIONS", ["scene"]): await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("scene")) == [] + client = await hass_client() orig_data = [ @@ -135,43 +118,40 @@ async def test_bad_formatted_scene(hass, hass_client, setup_scene): }, {"id": "light_off"}, ] + hass_config_store["scenes.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + await hass.async_block_till_done() - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.post( - "/api/config/scene/config/light_off", - data=json.dumps( - { - "id": "light_off", - "name": "Lights off", - "entities": {"light.bedroom": {"state": "off"}}, - } - ), - ) + assert sorted(hass.states.async_entity_ids("scene")) == [ + "scene.lights_off", + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} # Verify ID added to orig_data - assert "id" in orig_data[0] - - assert orig_data[1] == { - "id": "light_off", - "name": "Lights off", - "entities": {"light.bedroom": {"state": "off"}}, - } + assert hass_config_store["scenes.yaml"] == [ + { + "id": ANY, + "entities": {"light.bedroom": "on"}, + }, + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + }, + ] @pytest.mark.parametrize( @@ -183,7 +163,7 @@ async def test_bad_formatted_scene(hass, hass_client, setup_scene): ], ), ) -async def test_delete_scene(hass, hass_client, setup_scene): +async def test_delete_scene(hass, hass_client, hass_config_store, setup_scene): """Test deleting a scene.""" ent_reg = er.async_get(hass) @@ -192,31 +172,29 @@ async def test_delete_scene(hass, hass_client, setup_scene): with patch.object(config, "SECTIONS", ["scene"]): assert await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("scene")) == [ + "scene.light_off", + "scene.light_on", + ] + client = await hass_client() orig_data = [{"id": "light_on"}, {"id": "light_off"}] + hass_config_store["scenes.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data + resp = await client.delete("/api/config/scene/config/light_on") + await hass.async_block_till_done() - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): - resp = await client.delete("/api/config/scene/config/light_on") - await hass.async_block_till_done() + assert sorted(hass.states.async_entity_ids("scene")) == [ + "scene.light_off", + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - assert len(written) == 1 - assert written[0][0]["id"] == "light_off" + assert hass_config_store["scenes.yaml"] == [ + {"id": "light_off"}, + ] assert len(ent_reg.entities) == 1 diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 4b6ca1bdc8f..b8e980c29b9 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -26,38 +26,35 @@ async def setup_script(hass, script_config, stub_blueprint_populate): # noqa: F }, ), ) -async def test_delete_script(hass, hass_client): +async def test_delete_script(hass, hass_client, hass_config_store): """Test deleting a script.""" with patch.object(config, "SECTIONS", ["script"]): await async_setup_component(hass, "config", {}) + assert sorted(hass.states.async_entity_ids("script")) == [ + "script.one", + "script.two", + ] + ent_reg = er.async_get(hass) assert len(ent_reg.entities) == 2 client = await hass_client() orig_data = {"one": {}, "two": {}} + hass_config_store["scripts.yaml"] = orig_data - def mock_read(path): - """Mock reading data.""" - return orig_data + resp = await client.delete("/api/config/script/config/two") + await hass.async_block_till_done() - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ): - resp = await client.delete("/api/config/script/config/two") + assert sorted(hass.states.async_entity_ids("script")) == [ + "script.one", + ] assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} - assert len(written) == 1 - assert written[0] == {"one": {}} + assert hass_config_store["scripts.yaml"] == {"one": {}} assert len(ent_reg.entities) == 1 diff --git a/tests/components/cpuspeed/conftest.py b/tests/components/cpuspeed/conftest.py index a5d7d1837ba..be5a87b8d13 100644 --- a/tests/components/cpuspeed/conftest.py +++ b/tests/components/cpuspeed/conftest.py @@ -27,7 +27,7 @@ def mock_config_entry() -> MockConfigEntry: def mock_cpuinfo_config_flow() -> Generator[MagicMock, None, None]: """Return a mocked get_cpu_info. - It is only used to check thruthy or falsy values, so it is mocked + It is only used to check truthy or falsy values, so it is mocked to return True. """ with patch( diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 0d99d33e571..597d9282136 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -10,9 +10,13 @@ from pydeconz.models.sensor.presence import PresenceStatePresenceEvent from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.deconz_event import ( + ATTR_DURATION, + ATTR_ROTATION, CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT, CONF_DECONZ_PRESENCE_EVENT, + CONF_DECONZ_RELATIVE_ROTARY_EVENT, + RELATIVE_ROTARY_DECONZ_TO_EVENT, ) from homeassistant.const import ( CONF_DEVICE_ID, @@ -515,6 +519,105 @@ async def test_deconz_presence_events(hass, aioclient_mock, mock_deconz_websocke assert len(hass.states.async_all()) == 0 +async def test_deconz_relative_rotary_events( + hass, aioclient_mock, mock_deconz_websocket +): + """Test successful creation of deconz relative rotary events.""" + data = { + "sensors": { + "1": { + "config": { + "battery": 100, + "on": True, + "reachable": True, + }, + "etag": "463728970bdb7d04048fc4373654f45a", + "lastannounced": "2022-07-03T13:57:59Z", + "lastseen": "2022-07-03T14:02Z", + "manufacturername": "Signify Netherlands B.V.", + "modelid": "RDM002", + "name": "RDM002 44", + "state": { + "expectedeventduration": 400, + "expectedrotation": 75, + "lastupdated": "2022-07-03T11:37:49.586", + "rotaryevent": 2, + }, + "swversion": "2.59.19", + "type": "ZHARelativeRotary", + "uniqueid": "xx:xx:xx:xx:xx:xx:xx:xx-14-fc00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + device_registry = dr.async_get(hass) + + assert len(hass.states.async_all()) == 1 + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 3 + ) + + device = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, "xx:xx:xx:xx:xx:xx:xx:xx")} + ) + + captured_events = async_capture_events(hass, CONF_DECONZ_RELATIVE_ROTARY_EVENT) + + for rotary_event, duration, rotation in ((1, 100, 50), (2, 200, -50)): + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": { + "rotaryevent": rotary_event, + "expectedeventduration": duration, + "expectedrotation": rotation, + }, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 1 + assert captured_events[0].data == { + CONF_ID: "rdm002_44", + CONF_UNIQUE_ID: "xx:xx:xx:xx:xx:xx:xx:xx", + CONF_DEVICE_ID: device.id, + CONF_EVENT: RELATIVE_ROTARY_DECONZ_TO_EVENT[rotary_event], + ATTR_DURATION: duration, + ATTR_ROTATION: rotation, + } + captured_events.clear() + + # Unsupported relative rotary event + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "name": "123", + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert len(captured_events) == 0 + + await hass.config_entries.async_unload(config_entry.entry_id) + + states = hass.states.async_all() + assert len(hass.states.async_all()) == 1 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + async def test_deconz_events_bad_unique_id(hass, aioclient_mock): """Verify no devices are created if unique id is bad or missing.""" data = { diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 9ba0799d04e..1680854302b 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -7,6 +7,7 @@ from homeassistant.components.deconz.deconz_event import ( CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT, ) +from homeassistant.components.deconz.util import serial_from_unique_id from homeassistant.const import ( CONF_CODE, CONF_DEVICE_ID, @@ -60,7 +61,7 @@ async def test_humanifying_deconz_alarm_event(hass, aioclient_mock): device_registry = dr.async_get(hass) keypad_event_id = slugify(data["sensors"]["1"]["name"]) - keypad_serial = data["sensors"]["1"]["uniqueid"].split("-", 1)[0] + keypad_serial = serial_from_unique_id(data["sensors"]["1"]["uniqueid"]) keypad_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, keypad_serial)} ) @@ -131,25 +132,25 @@ async def test_humanifying_deconz_event(hass, aioclient_mock): device_registry = dr.async_get(hass) switch_event_id = slugify(data["sensors"]["1"]["name"]) - switch_serial = data["sensors"]["1"]["uniqueid"].split("-", 1)[0] + switch_serial = serial_from_unique_id(data["sensors"]["1"]["uniqueid"]) switch_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, switch_serial)} ) hue_remote_event_id = slugify(data["sensors"]["2"]["name"]) - hue_remote_serial = data["sensors"]["2"]["uniqueid"].split("-", 1)[0] + hue_remote_serial = serial_from_unique_id(data["sensors"]["2"]["uniqueid"]) hue_remote_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, hue_remote_serial)} ) xiaomi_cube_event_id = slugify(data["sensors"]["3"]["name"]) - xiaomi_cube_serial = data["sensors"]["3"]["uniqueid"].split("-", 1)[0] + xiaomi_cube_serial = serial_from_unique_id(data["sensors"]["3"]["uniqueid"]) xiaomi_cube_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, xiaomi_cube_serial)} ) faulty_event_id = slugify(data["sensors"]["4"]["name"]) - faulty_serial = data["sensors"]["4"]["uniqueid"].split("-", 1)[0] + faulty_serial = serial_from_unique_id(data["sensors"]["4"]["uniqueid"]) faulty_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, faulty_serial)} ) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index ac8100caa3d..d877e63864a 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -100,12 +100,13 @@ TEST_DATA = [ "old_unique_id": "00:12:4b:00:14:4d:00:07-ppb", "state": "809", "entity_category": None, - "device_class": SensorDeviceClass.AQI, + "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, "state_class": SensorStateClass.MEASUREMENT, "attributes": { - "state_class": "measurement", - "device_class": "aqi", + "device_class": "volatile_organic_compounds", "friendly_name": "BOSCH Air quality sensor PPB", + "state_class": "measurement", + "unit_of_measurement": "ppb", }, "websocket_event": {"state": {"airqualityppb": 1000}}, "next_state": "1000", diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index c1b9d4c436e..e5156f35317 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -63,6 +63,7 @@ async def test_demo_statistics(recorder_mock, mock_history, hass): list_statistic_ids, hass ) assert { + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": "Outdoor temperature", @@ -72,6 +73,7 @@ async def test_demo_statistics(recorder_mock, mock_history, hass): "unit_class": "temperature", } in statistic_ids assert { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": "Energy consumption 1", @@ -114,7 +116,7 @@ async def test_demo_statistics_growth(recorder_mock, mock_history, hass): await async_wait_recording_done(hass) statistics = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, False + get_last_statistics, hass, 1, statistic_id, False, {"sum"} ) assert statistics[statistic_id][0]["sum"] > 2**20 assert statistics[statistic_id][0]["sum"] <= (2**20 + 24) @@ -192,6 +194,20 @@ async def test_issues_created(mock_history, hass, hass_client, hass_ws_client): "translation_key": "bad_psu", "translation_placeholders": None, }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "is_fixable": True, + "issue_domain": None, + "issue_id": "cold_tea", + "learn_more_url": None, + "severity": "warning", + "translation_key": "cold_tea", + "translation_placeholders": None, + "ignored": False, + }, ] } @@ -280,5 +296,19 @@ async def test_issues_created(mock_history, hass, hass_client, hass_ws_client): "translation_key": "bad_psu", "translation_placeholders": None, }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "is_fixable": True, + "issue_domain": None, + "issue_id": "cold_tea", + "learn_more_url": None, + "severity": "warning", + "translation_key": "cold_tea", + "translation_placeholders": None, + "ignored": False, + }, ] } diff --git a/tests/components/demo/test_text.py b/tests/components/demo/test_text.py new file mode 100644 index 00000000000..df93531bd1a --- /dev/null +++ b/tests/components/demo/test_text.py @@ -0,0 +1,44 @@ +"""The tests for the demo text component.""" +import pytest + +from homeassistant.components.text import ( + ATTR_MAX, + ATTR_MIN, + ATTR_PATTERN, + ATTR_VALUE, + DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, MAX_LENGTH_STATE_STATE +from homeassistant.setup import async_setup_component + +ENTITY_TEXT = "text.text" + + +@pytest.fixture(autouse=True) +async def setup_demo_text(hass): + """Initialize setup demo text.""" + assert await async_setup_component(hass, DOMAIN, {"text": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass): + """Test the initial parameters.""" + state = hass.states.get(ENTITY_TEXT) + assert state.state == "Hello world" + assert state.attributes[ATTR_MIN] == 0 + assert state.attributes[ATTR_MAX] == MAX_LENGTH_STATE_STATE + assert state.attributes[ATTR_PATTERN] is None + assert state.attributes[ATTR_MODE] == "text" + + +async def test_set_value(hass): + """Test set value service.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_TEXT, ATTR_VALUE: "new"}, + blocking=True, + ) + state = hass.states.get(ENTITY_TEXT) + assert state.state == "new" diff --git a/tests/components/devolo_home_control/test_binary_sensor.py b/tests/components/devolo_home_control/test_binary_sensor.py index 4bce2ebd19e..ec8a7d288ab 100644 --- a/tests/components/devolo_home_control/test_binary_sensor.py +++ b/tests/components/devolo_home_control/test_binary_sensor.py @@ -4,7 +4,12 @@ from unittest.mock import patch import pytest from homeassistant.components.binary_sensor import DOMAIN -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory @@ -31,25 +36,30 @@ async def test_binary_sensor(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(f"{DOMAIN}.test") + state = hass.states.get(f"{DOMAIN}.test_door") assert state is not None assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test Door" - state = hass.states.get(f"{DOMAIN}.test_2") + state = hass.states.get(f"{DOMAIN}.test_overload") assert state is not None + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test Overload" er = entity_registry.async_get(hass) - assert er.async_get(f"{DOMAIN}.test_2").entity_category == EntityCategory.DIAGNOSTIC + assert ( + er.async_get(f"{DOMAIN}.test_overload").entity_category + == EntityCategory.DIAGNOSTIC + ) # Emulate websocket message: sensor turned on test_gateway.publisher.dispatch("Test", ("Test", True)) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON + assert hass.states.get(f"{DOMAIN}.test_door").state == STATE_ON # Emulate websocket message: device went offline test_gateway.devices["Test"].status = 1 test_gateway.publisher.dispatch("Test", ("Status", False, "status")) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + assert hass.states.get(f"{DOMAIN}.test_door").state == STATE_UNAVAILABLE @pytest.mark.usefixtures("mock_zeroconf") @@ -65,25 +75,26 @@ async def test_remote_control(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(f"{DOMAIN}.test") + state = hass.states.get(f"{DOMAIN}.test_button_1") assert state is not None assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test Button 1" # Emulate websocket message: button pressed test_gateway.publisher.dispatch("Test", ("Test", 1)) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON + assert hass.states.get(f"{DOMAIN}.test_button_1").state == STATE_ON # Emulate websocket message: button released test_gateway.publisher.dispatch("Test", ("Test", 0)) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.test").state == STATE_OFF + assert hass.states.get(f"{DOMAIN}.test_button_1").state == STATE_OFF # Emulate websocket message: device went offline test_gateway.devices["Test"].status = 1 test_gateway.publisher.dispatch("Test", ("Status", False, "status")) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + assert hass.states.get(f"{DOMAIN}.test_button_1").state == STATE_UNAVAILABLE @pytest.mark.usefixtures("mock_zeroconf") @@ -97,7 +108,7 @@ async def test_disabled(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.devolo.WarningBinaryFI:Test") is None + assert hass.states.get(f"{DOMAIN}.test_door") is None @pytest.mark.usefixtures("mock_zeroconf") @@ -112,7 +123,7 @@ async def test_remove_from_hass(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(f"{DOMAIN}.test") + state = hass.states.get(f"{DOMAIN}.test_door") assert state is not None await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/devolo_home_control/test_climate.py b/tests/components/devolo_home_control/test_climate.py index 98200b66476..c1ecc7ecc29 100644 --- a/tests/components/devolo_home_control/test_climate.py +++ b/tests/components/devolo_home_control/test_climate.py @@ -7,7 +7,12 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_TEMPERATURE, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from . import configure_integration @@ -30,6 +35,7 @@ async def test_climate(hass: HomeAssistant): assert state is not None assert state.state == HVACMode.HEAT assert state.attributes[ATTR_TEMPERATURE] == test_gateway.devices["Test"].value + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test" # Emulate websocket message: temperature changed test_gateway.publisher.dispatch("Test", ("Test", 21.0)) diff --git a/tests/components/devolo_home_control/test_cover.py b/tests/components/devolo_home_control/test_cover.py index 1c05c00370b..a783b635001 100644 --- a/tests/components/devolo_home_control/test_cover.py +++ b/tests/components/devolo_home_control/test_cover.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant.components.cover import ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, @@ -32,6 +33,7 @@ async def test_cover(hass: HomeAssistant): state = hass.states.get(f"{DOMAIN}.test") assert state is not None assert state.state == STATE_OPEN + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test" assert ( state.attributes[ATTR_CURRENT_POSITION] == test_gateway.devices["Test"] diff --git a/tests/components/devolo_home_control/test_light.py b/tests/components/devolo_home_control/test_light.py index 7b18b28a493..ce4a42f5226 100644 --- a/tests/components/devolo_home_control/test_light.py +++ b/tests/components/devolo_home_control/test_light.py @@ -10,6 +10,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -36,6 +37,7 @@ async def test_light_without_binary_sensor(hass: HomeAssistant): state = hass.states.get(f"{DOMAIN}.test") assert state is not None assert state.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test" assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] assert state.attributes[ATTR_BRIGHTNESS] == round( diff --git a/tests/components/devolo_home_control/test_siren.py b/tests/components/devolo_home_control/test_siren.py index 97e044738a5..bca54a5b5b8 100644 --- a/tests/components/devolo_home_control/test_siren.py +++ b/tests/components/devolo_home_control/test_siren.py @@ -4,7 +4,12 @@ from unittest.mock import patch import pytest from homeassistant.components.siren import DOMAIN -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from . import configure_integration @@ -27,6 +32,7 @@ async def test_siren(hass: HomeAssistant): state = hass.states.get(f"{DOMAIN}.test") assert state is not None assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test" # Emulate websocket message: sensor turned on test_gateway.publisher.dispatch("Test", ("devolo.SirenMultiLevelSwitch:Test", 1)) diff --git a/tests/components/discord/conftest.py b/tests/components/discord/conftest.py new file mode 100644 index 00000000000..c98944fdc85 --- /dev/null +++ b/tests/components/discord/conftest.py @@ -0,0 +1,45 @@ +"""Discord notification test helpers.""" +from http import HTTPStatus + +import pytest + +from homeassistant.components.discord.notify import DiscordNotificationService +from homeassistant.core import HomeAssistant + +from tests.test_util.aiohttp import AiohttpClientMocker + +MESSAGE = "Testing Discord Messenger platform" +CONTENT = b"TestContent" +URL_ATTACHMENT = "http://127.0.0.1:8080/image.jpg" +TARGET = "1234567890" + + +@pytest.fixture +def discord_notification_service(hass: HomeAssistant) -> DiscordNotificationService: + """Set up discord notification service.""" + hass.config.allowlist_external_urls.add(URL_ATTACHMENT) + return DiscordNotificationService(hass, "token") + + +@pytest.fixture +def discord_aiohttp_mock_factory( + aioclient_mock: AiohttpClientMocker, +) -> AiohttpClientMocker: + """Create Discord service mock from factory.""" + + def _discord_aiohttp_mock_factory( + headers: dict[str, str] = None, + ) -> AiohttpClientMocker: + if headers is not None: + aioclient_mock.get( + URL_ATTACHMENT, status=HTTPStatus.OK, content=CONTENT, headers=headers + ) + else: + aioclient_mock.get( + URL_ATTACHMENT, + status=HTTPStatus.OK, + content=CONTENT, + ) + return aioclient_mock + + return _discord_aiohttp_mock_factory diff --git a/tests/components/discord/test_notify.py b/tests/components/discord/test_notify.py new file mode 100644 index 00000000000..810898cdf73 --- /dev/null +++ b/tests/components/discord/test_notify.py @@ -0,0 +1,96 @@ +"""Test Discord notify.""" +import logging + +import pytest + +from homeassistant.components.discord.notify import DiscordNotificationService + +from .conftest import CONTENT, MESSAGE, URL_ATTACHMENT + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_send_message_without_target_logs_error( + discord_notification_service: DiscordNotificationService, + discord_aiohttp_mock_factory: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test send message.""" + discord_aiohttp_mock = discord_aiohttp_mock_factory() + with caplog.at_level( + logging.ERROR, logger="homeassistant.components.discord.notify" + ): + await discord_notification_service.async_send_message(MESSAGE) + assert "No target specified" in caplog.text + assert discord_aiohttp_mock.call_count == 0 + + +async def test_get_file_from_url( + discord_notification_service: DiscordNotificationService, + discord_aiohttp_mock_factory: AiohttpClientMocker, +) -> None: + """Test getting a file from a URL.""" + headers = {"Content-Length": str(len(CONTENT))} + discord_aiohttp_mock = discord_aiohttp_mock_factory(headers) + result = await discord_notification_service.async_get_file_from_url( + URL_ATTACHMENT, True, len(CONTENT) + ) + + assert discord_aiohttp_mock.call_count == 1 + assert result == bytearray(CONTENT) + + +async def test_get_file_from_url_not_on_allowlist( + discord_notification_service: DiscordNotificationService, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test getting file from URL that isn't on the allowlist.""" + url = "http://dodgyurl.com" + with caplog.at_level( + logging.WARNING, logger="homeassistant.components.discord.notify" + ): + result = await discord_notification_service.async_get_file_from_url( + url, True, len(CONTENT) + ) + + assert f"URL not allowed: {url}" in caplog.text + assert result is None + + +async def test_get_file_from_url_with_large_attachment( + discord_notification_service: DiscordNotificationService, + discord_aiohttp_mock_factory: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test getting file from URL with large attachment (per Content-Length header) throws error.""" + headers = {"Content-Length": str(len(CONTENT) + 1)} + discord_aiohttp_mock = discord_aiohttp_mock_factory(headers) + with caplog.at_level( + logging.WARNING, logger="homeassistant.components.discord.notify" + ): + result = await discord_notification_service.async_get_file_from_url( + URL_ATTACHMENT, True, len(CONTENT) + ) + + assert discord_aiohttp_mock.call_count == 1 + assert "Attachment too large (Content-Length reports" in caplog.text + assert result is None + + +async def test_get_file_from_url_with_large_attachment_no_header( + discord_notification_service: DiscordNotificationService, + discord_aiohttp_mock_factory: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test getting file from URL with large attachment (per content length) throws error.""" + discord_aiohttp_mock = discord_aiohttp_mock_factory() + with caplog.at_level( + logging.WARNING, logger="homeassistant.components.discord.notify" + ): + result = await discord_notification_service.async_get_file_from_url( + URL_ATTACHMENT, True, len(CONTENT) - 1 + ) + + assert discord_aiohttp_mock.call_count == 1 + assert "Attachment too large (Stream reports" in caplog.text + assert result is None diff --git a/tests/components/dnsip/__init__.py b/tests/components/dnsip/__init__.py index 9fb6f529c5e..1a465b59ab6 100644 --- a/tests/components/dnsip/__init__.py +++ b/tests/components/dnsip/__init__.py @@ -1 +1,37 @@ """Tests for the dnsip integration.""" +from __future__ import annotations + + +class QueryResult: + """Return Query results.""" + + host = "1.2.3.4" + ttl = 60 + + +class RetrieveDNS: + """Return list of test information.""" + + def __init__( + self, nameservers: list[str] | None = None, error: Exception | None = None + ) -> None: + """Initialize DNS class.""" + if nameservers: + self.nameservers = nameservers + self._nameservers = ["1.2.3.4"] + self.error = error + + async def query(self, hostname, qtype) -> dict[str, str]: + """Return information.""" + if self.error: + raise self.error + return [QueryResult] + + @property + def nameservers(self) -> list[str]: + """Return nameserver.""" + return self._nameservers + + @nameservers.setter + def nameservers(self, value: list[str]) -> None: + self._nameservers = value diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index fdec45be7f5..990fd4df159 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -20,23 +20,11 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import RetrieveDNS + from tests.common import MockConfigEntry -class RetrieveDNS: - """Return list of test information.""" - - @staticmethod - async def query(hostname, qtype) -> dict[str, str]: - """Return information.""" - return {"hostname": "1.2.3.4"} - - @property - def nameservers(self) -> list[str]: - """Return nameserver.""" - return ["1.2.3.4"] - - async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -164,12 +152,13 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) + dns_mock = RetrieveDNS() with patch( "homeassistant.components.dnsip.async_setup_entry", return_value=True, ), patch( "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", - return_value=RetrieveDNS, + return_value=dns_mock, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -204,9 +193,6 @@ async def test_options_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", return_value=RetrieveDNS(), - ), patch( - "homeassistant.components.dnsip.async_setup_entry", - return_value=True, ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -223,6 +209,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RESOLVER_IPV6: "2001:4860:4860::8888", }, ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { @@ -230,6 +217,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: "resolver_ipv6": "2001:4860:4860::8888", } + assert entry.state == config_entries.ConfigEntryState.LOADED + @pytest.mark.parametrize( "p_input", diff --git a/tests/components/dnsip/test_init.py b/tests/components/dnsip/test_init.py new file mode 100644 index 00000000000..1c8cf04c783 --- /dev/null +++ b/tests/components/dnsip/test_init.py @@ -0,0 +1,54 @@ +"""Test for DNS IP component Init.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.dnsip.const import ( + CONF_HOSTNAME, + CONF_IPV4, + CONF_IPV6, + CONF_RESOLVER, + CONF_RESOLVER_IPV6, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant + +from . import RetrieveDNS + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry(hass: HomeAssistant) -> None: + """Test load and unload an entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_IPV4: True, + CONF_IPV6: False, + }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + }, + entry_id="1", + unique_id="home-assistant.io", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/dnsip/test_sensor.py b/tests/components/dnsip/test_sensor.py new file mode 100644 index 00000000000..f44d58d2125 --- /dev/null +++ b/tests/components/dnsip/test_sensor.py @@ -0,0 +1,105 @@ +"""The test for the DNS IP sensor platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from aiodns.error import DNSError + +from homeassistant.components.dnsip.const import ( + CONF_HOSTNAME, + CONF_IPV4, + CONF_IPV6, + CONF_RESOLVER, + CONF_RESOLVER_IPV6, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from . import RetrieveDNS + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_sensor(hass: HomeAssistant) -> None: + """Test the DNS IP sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_IPV4: True, + CONF_IPV6: True, + }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + }, + entry_id="1", + unique_id="home-assistant.io", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dnsip.sensor.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.home_assistant_io") + state2 = hass.states.get("sensor.home_assistant_io_ipv6") + + assert state1.state == "1.2.3.4" + assert state2.state == "1.2.3.4" + + +async def test_sensor_no_response(hass: HomeAssistant) -> None: + """Test the DNS IP sensor with DNS error.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_IPV4: True, + CONF_IPV6: False, + }, + options={ + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + }, + entry_id="1", + unique_id="home-assistant.io", + ) + entry.add_to_hass(hass) + + dns_mock = RetrieveDNS() + with patch( + "homeassistant.components.dnsip.sensor.aiodns.DNSResolver", + return_value=dns_mock, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_assistant_io") + + assert state.state == "1.2.3.4" + + dns_mock.error = DNSError() + with patch( + "homeassistant.components.dnsip.sensor.aiodns.DNSResolver", + return_value=dns_mock, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=10), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_assistant_io") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 363a9671f59..f5cfaec7a97 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -70,10 +70,12 @@ async def test_add_devices_then_register(hass): device1.category = "light" device1.name = "NAME" device1.unique_id = "unique1" + device1.brightness = 1 device2 = Mock() device2.category = "switch" device2.name = "NAME2" device2.unique_id = "unique2" + device2.brightness = 1 new_device_func([device1, device2]) device3 = Mock() device3.category = "switch" @@ -103,10 +105,12 @@ async def test_register_then_add_devices(hass): device1.category = "light" device1.name = "NAME" device1.unique_id = "unique1" + device1.brightness = 1 device2 = Mock() device2.category = "switch" device2.name = "NAME2" device2.unique_id = "unique2" + device2.brightness = 1 new_device_func([device1, device2]) await hass.async_block_till_done() assert hass.states.get("light.name") diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py index fd671365ba1..5fbb22b91a7 100644 --- a/tests/components/dynalite/test_cover.py +++ b/tests/components/dynalite/test_cover.py @@ -1,8 +1,25 @@ """Test Dynalite cover.""" +from unittest.mock import Mock + from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice import pytest -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDeviceClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import State from homeassistant.exceptions import HomeAssistantError from .common import ( @@ -14,12 +31,25 @@ from .common import ( run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" mock_dev = create_mock_device("cover", DynaliteTimeCoverWithTiltDevice) - mock_dev.device_class = "blind" + mock_dev.device_class = CoverDeviceClass.BLIND.value + mock_dev.current_cover_position = 0 + mock_dev.current_cover_tilt_position = 0 + mock_dev.is_opening = False + mock_dev.is_closing = False + mock_dev.is_closed = True + + def mock_init_level(target): + mock_dev.is_closed = target == 0 + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev @@ -29,11 +59,11 @@ async def test_cover_setup(hass, mock_device): entity_state = hass.states.get("cover.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name assert ( - entity_state.attributes["current_position"] + entity_state.attributes[ATTR_CURRENT_POSITION] == mock_device.current_cover_position ) assert ( - entity_state.attributes["current_tilt_position"] + entity_state.attributes[ATTR_CURRENT_TILT_POSITION] == mock_device.current_cover_tilt_position ) assert entity_state.attributes[ATTR_DEVICE_CLASS] == mock_device.device_class @@ -48,7 +78,7 @@ async def test_cover_setup(hass, mock_device): { ATTR_SERVICE: "set_cover_position", ATTR_METHOD: "async_set_cover_position", - ATTR_ARGS: {"position": 50}, + ATTR_ARGS: {ATTR_POSITION: 50}, }, {ATTR_SERVICE: "open_cover_tilt", ATTR_METHOD: "async_open_cover_tilt"}, {ATTR_SERVICE: "close_cover_tilt", ATTR_METHOD: "async_close_cover_tilt"}, @@ -56,7 +86,7 @@ async def test_cover_setup(hass, mock_device): { ATTR_SERVICE: "set_cover_tilt_position", ATTR_METHOD: "async_set_cover_tilt_position", - ATTR_ARGS: {"tilt_position": 50}, + ATTR_ARGS: {ATTR_TILT_POSITION: 50}, }, ], ) @@ -91,14 +121,38 @@ async def test_cover_positions(hass, mock_device): """Test that the state updates in the various positions.""" update_func = await create_entity_from_device(hass, mock_device) await check_cover_position( - hass, update_func, mock_device, True, False, False, "closing" + hass, update_func, mock_device, True, False, False, STATE_CLOSING ) await check_cover_position( - hass, update_func, mock_device, False, True, False, "opening" + hass, update_func, mock_device, False, True, False, STATE_OPENING ) await check_cover_position( - hass, update_func, mock_device, False, False, True, "closed" + hass, update_func, mock_device, False, False, True, STATE_CLOSED ) await check_cover_position( - hass, update_func, mock_device, False, False, False, "open" + hass, update_func, mock_device, False, False, False, STATE_OPEN ) + + +async def test_cover_restore_state(hass, mock_device): + """Test restore from cache.""" + mock_restore_cache( + hass, + [State("cover.name", STATE_OPEN, attributes={ATTR_CURRENT_POSITION: 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(77) + entity_state = hass.states.get("cover.name") + assert entity_state.state == STATE_OPEN + + +async def test_cover_restore_state_bad_cache(hass, mock_device): + """Test restore from a cache without the attribute.""" + mock_restore_cache( + hass, + [State("cover.name", STATE_OPEN, attributes={"bla bla": 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_not_called() + entity_state = hass.states.get("cover.name") + assert entity_state.state == STATE_CLOSED diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index b100cf8d3f6..337f0a415e6 100644 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -1,8 +1,11 @@ """Test Dynalite light.""" +from unittest.mock import Mock, PropertyMock + from dynalite_devices_lib.light import DynaliteChannelLightDevice import pytest from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_SUPPORTED_COLOR_MODES, ColorMode, @@ -10,8 +13,11 @@ from homeassistant.components.light import ( from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import State from .common import ( ATTR_METHOD, @@ -22,11 +28,25 @@ from .common import ( run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" - return create_mock_device("light", DynaliteChannelLightDevice) + mock_dev = create_mock_device("light", DynaliteChannelLightDevice) + mock_dev.brightness = 0 + + def mock_is_on(): + return mock_dev.brightness != 0 + + type(mock_dev).is_on = PropertyMock(side_effect=mock_is_on) + + def mock_init_level(target): + mock_dev.brightness = target + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev async def test_light_setup(hass, mock_device): @@ -34,10 +54,9 @@ async def test_light_setup(hass, mock_device): await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("light.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name - assert entity_state.attributes["brightness"] == mock_device.brightness - assert entity_state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS assert entity_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] assert entity_state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert entity_state.state == STATE_OFF await run_service_tests( hass, mock_device, @@ -67,3 +86,29 @@ async def test_remove_config_entry(hass, mock_device): assert await hass.config_entries.async_remove(entry_id) await hass.async_block_till_done() assert not hass.states.get("light.name") + + +async def test_light_restore_state(hass, mock_device): + """Test restore from cache.""" + mock_restore_cache( + hass, + [State("light.name", STATE_ON, attributes={ATTR_BRIGHTNESS: 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(77) + entity_state = hass.states.get("light.name") + assert entity_state.state == STATE_ON + assert entity_state.attributes[ATTR_BRIGHTNESS] == 77 + assert entity_state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + + +async def test_light_restore_state_bad_cache(hass, mock_device): + """Test restore from a cache without the attribute.""" + mock_restore_cache( + hass, + [State("light.name", "abc", attributes={"blabla": 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_not_called() + entity_state = hass.states.get("light.name") + assert entity_state.state == STATE_OFF diff --git a/tests/components/dynalite/test_switch.py b/tests/components/dynalite/test_switch.py index de375e3b348..95ab64ef197 100644 --- a/tests/components/dynalite/test_switch.py +++ b/tests/components/dynalite/test_switch.py @@ -1,9 +1,12 @@ """Test Dynalite switch.""" +from unittest.mock import Mock + from dynalite_devices_lib.switch import DynalitePresetSwitchDevice import pytest -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON +from homeassistant.core import State from .common import ( ATTR_METHOD, @@ -13,11 +16,20 @@ from .common import ( run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" - return create_mock_device("switch", DynalitePresetSwitchDevice) + mock_dev = create_mock_device("switch", DynalitePresetSwitchDevice) + mock_dev.is_on = False + + def mock_init_level(level): + mock_dev.is_on = level + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev async def test_switch_setup(hass, mock_device): @@ -25,6 +37,7 @@ async def test_switch_setup(hass, mock_device): await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("switch.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name + assert entity_state.state == STATE_OFF await run_service_tests( hass, mock_device, @@ -34,3 +47,21 @@ async def test_switch_setup(hass, mock_device): {ATTR_SERVICE: "turn_off", ATTR_METHOD: "async_turn_off"}, ], ) + + +@pytest.mark.parametrize("saved_state, level", [(STATE_ON, 1), (STATE_OFF, 0)]) +async def test_switch_restore_state(hass, mock_device, saved_state, level): + """Test restore from cache.""" + mock_restore_cache( + hass, + [ + State( + "switch.name", + saved_state, + ) + ], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(level) + entity_state = hass.states.get("switch.name") + assert entity_state.state == saved_state diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 408a44cda00..cfe4dd3a7bb 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components.emulated_hue.config import ( SAVE_DELAY, Config, ) +from homeassistant.components.emulated_hue.upnp import UPNPResponderProtocol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component from homeassistant.util import utcnow @@ -127,6 +128,9 @@ async def test_setup_works(hass): ) as mock_create_upnp_datagram_endpoint, patch( "homeassistant.components.emulated_hue.async_get_source_ip" ): + mock_create_upnp_datagram_endpoint.return_value = AsyncMock( + spec=UPNPResponderProtocol + ) assert await async_setup_component(hass, "emulated_hue", {}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index ce7f013963c..770f636098c 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -31,7 +31,7 @@ class MockTransport: @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 5afee6f6cc3..8adf1a18f19 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -25,7 +25,7 @@ async def test_events_fired_properly(hass): roku_event_handler = None def instantiate( - loop, + event_loop, handler, roku_usn, host_ip, diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 0108dd1de76..636eb74e4d3 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -3,6 +3,7 @@ import copy from datetime import timedelta from unittest.mock import patch +from freezegun import freeze_time import pytest from homeassistant.components.energy import data @@ -41,6 +42,13 @@ async def setup_integration(recorder_mock): return setup_integration +@pytest.fixture(autouse=True) +@freeze_time("2022-04-19 07:53:05") +def frozen_time(): + """Freeze clock for tests.""" + yield + + def get_statistics_for_entity(statistics_results, entity_id): """Get statistics for a certain entity, or None if there is none.""" for statistics_result in statistics_results: diff --git a/tests/components/environment_canada/fixtures/config_entry_data.json b/tests/components/environment_canada/fixtures/config_entry_data.json new file mode 100644 index 00000000000..085a3394dce --- /dev/null +++ b/tests/components/environment_canada/fixtures/config_entry_data.json @@ -0,0 +1,110 @@ +{ + "config_entry_data": { + "latitude": "**REDACTED**", + "longitude": "**REDACTED**", + "station": "XX/1234567", + "language": "Gibberish" + }, + "weather_data": { + "temperature": { + "label": "Temperature", + "value": 14.9, + "unit": "C" + }, + "dewpoint": { + "label": "Dew Point", + "value": 1.4, + "unit": "C" + }, + "wind_chill": { + "label": "Wind Chill", + "value": null + }, + "humidex": { + "label": "Humidex", + "value": null + }, + "pressure": { + "label": "Pressure", + "value": 102.7, + "unit": "kPa" + }, + "tendency": { + "label": "Tendency", + "value": "falling" + }, + "humidity": { + "label": "Humidity", + "value": 40, + "unit": "%" + }, + "visibility": { + "label": "Visibility", + "value": 24.1, + "unit": "km" + }, + "condition": { + "label": "Condition", + "value": "Mainly Sunny" + }, + "wind_speed": { + "label": "Wind Speed", + "value": 1, + "unit": "km/h" + }, + "wind_gust": { + "label": "Wind Gust", + "value": null + }, + "wind_dir": { + "label": "Wind Direction", + "value": "N" + }, + "wind_bearing": { + "label": "Wind Bearing", + "value": 0, + "unit": "degrees" + }, + "high_temp": { + "label": "High Temperature", + "value": 18, + "unit": "C" + }, + "low_temp": { + "label": "Low Temperature", + "value": -1, + "unit": "C" + }, + "uv_index": { + "label": "UV Index", + "value": 5 + }, + "pop": { + "label": "Chance of Precip.", + "value": null + }, + "icon_code": { + "label": "Icon Code", + "value": "01" + }, + "precip_yesterday": { + "label": "Precipitation Yesterday", + "value": 0.0, + "unit": "mm" + }, + "normal_high": { + "label": "Normal High Temperature", + "value": 15, + "unit": "C" + }, + "normal_low": { + "label": "Normal Low Temperature", + "value": 6, + "unit": "C" + }, + "text_summary": { + "label": "Forecast", + "value": "Tonight. Clear. Fog patches developing after midnight. Low minus 1 with frost." + } + } +} diff --git a/tests/components/environment_canada/fixtures/current_conditions_data.json b/tests/components/environment_canada/fixtures/current_conditions_data.json new file mode 100644 index 00000000000..f3a18869940 --- /dev/null +++ b/tests/components/environment_canada/fixtures/current_conditions_data.json @@ -0,0 +1,235 @@ +{ + "alerts": { + "warnings": { + "value": [], + "label": "Warnings" + }, + "watches": { + "value": [], + "label": "Watches" + }, + "advisories": { + "value": [ + { + "title": "Frost Advisory", + "date": "Monday October 03, 2022 at 15:05 EDT" + } + ], + "label": "Advisories" + }, + "statements": { + "value": [], + "label": "Statements" + }, + "endings": { + "value": [], + "label": "Endings" + } + }, + "conditions": { + "temperature": { + "label": "Temperature", + "value": 14.9, + "unit": "C" + }, + "dewpoint": { + "label": "Dew Point", + "value": 1.4, + "unit": "C" + }, + "wind_chill": { + "label": "Wind Chill", + "value": null + }, + "humidex": { + "label": "Humidex", + "value": null + }, + "pressure": { + "label": "Pressure", + "value": 102.7, + "unit": "kPa" + }, + "tendency": { + "label": "Tendency", + "value": "falling" + }, + "humidity": { + "label": "Humidity", + "value": 40, + "unit": "%" + }, + "visibility": { + "label": "Visibility", + "value": 24.1, + "unit": "km" + }, + "condition": { + "label": "Condition", + "value": "Mainly Sunny" + }, + "wind_speed": { + "label": "Wind Speed", + "value": 1, + "unit": "km/h" + }, + "wind_gust": { + "label": "Wind Gust", + "value": null + }, + "wind_dir": { + "label": "Wind Direction", + "value": "N" + }, + "wind_bearing": { + "label": "Wind Bearing", + "value": 0, + "unit": "degrees" + }, + "high_temp": { + "label": "High Temperature", + "value": 18, + "unit": "C" + }, + "low_temp": { + "label": "Low Temperature", + "value": -1, + "unit": "C" + }, + "uv_index": { + "label": "UV Index", + "value": 5 + }, + "pop": { + "label": "Chance of Precip.", + "value": null + }, + "icon_code": { + "label": "Icon Code", + "value": "01" + }, + "precip_yesterday": { + "label": "Precipitation Yesterday", + "value": 0.0, + "unit": "mm" + }, + "normal_high": { + "label": "Normal High Temperature", + "value": 15, + "unit": "C" + }, + "normal_low": { + "label": "Normal Low Temperature", + "value": 6, + "unit": "C" + }, + "text_summary": { + "label": "Forecast", + "value": "Tonight. Clear. Fog patches developing after midnight. Low minus 1 with frost." + } + }, + "daily_forecasts": [ + { + "period": "Monday night", + "text_summary": "Clear. Fog patches developing after midnight. Low minus 1 with frost.", + "icon_code": "30", + "temperature": -1, + "temperature_class": "low", + "precip_probability": 0 + }, + { + "period": "Tuesday", + "text_summary": "Sunny. Fog patches dissipating in the morning. High 18. UV index 5 or moderate.", + "icon_code": "00", + "temperature": 18, + "temperature_class": "high", + "precip_probability": 0 + }, + { + "period": "Tuesday night", + "text_summary": "Clear. Fog patches developing overnight. Low plus 3.", + "icon_code": "30", + "temperature": 3, + "temperature_class": "low", + "precip_probability": 0 + }, + { + "period": "Wednesday", + "text_summary": "Sunny. High 20.", + "icon_code": "00", + "temperature": 20, + "temperature_class": "high", + "precip_probability": 0 + }, + { + "period": "Wednesday night", + "text_summary": "Clear. Low 9.", + "icon_code": "30", + "temperature": 9, + "temperature_class": "low", + "precip_probability": 0 + }, + { + "period": "Thursday", + "text_summary": "A mix of sun and cloud. High 20.", + "icon_code": "02", + "temperature": 20, + "temperature_class": "high", + "precip_probability": 0 + }, + { + "period": "Thursday night", + "text_summary": "Showers. Low 7.", + "icon_code": "12", + "temperature": 7, + "temperature_class": "low", + "precip_probability": 0 + }, + { + "period": "Friday", + "text_summary": "Cloudy with 40 percent chance of showers. High 13.", + "icon_code": "12", + "temperature": 13, + "temperature_class": "high", + "precip_probability": 40 + }, + { + "period": "Friday night", + "text_summary": "Cloudy periods. Low plus 1.", + "icon_code": "32", + "temperature": 1, + "temperature_class": "low", + "precip_probability": 0 + }, + { + "period": "Saturday", + "text_summary": "A mix of sun and cloud. High 10.", + "icon_code": "02", + "temperature": 10, + "temperature_class": "high", + "precip_probability": 0 + }, + { + "period": "Saturday night", + "text_summary": "Cloudy periods. Low plus 3.", + "icon_code": "32", + "temperature": 3, + "temperature_class": "low", + "precip_probability": 0 + }, + { + "period": "Sunday", + "text_summary": "A mix of sun and cloud. High 12.", + "icon_code": "02", + "temperature": 12, + "temperature_class": "high", + "precip_probability": 0 + } + ], + "metadata": { + "attribution": "Data provided by Environment Canada", + "timestamp": "2022/10/3", + "location": "Ottawa (Kanata - Orl\u00e9ans)", + "station": "Ottawa Macdonald-Cartier Intl Airport" + } +} diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py new file mode 100644 index 00000000000..a1f3539a5e4 --- /dev/null +++ b/tests/components/environment_canada/test_diagnostics.py @@ -0,0 +1,85 @@ +"""Test Environment Canada diagnostics.""" + +from datetime import datetime, timezone +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant.components.environment_canada.const import ( + CONF_LANGUAGE, + CONF_STATION, + DOMAIN, +) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.components.diagnostics import get_diagnostics_for_config_entry + +FIXTURE_USER_INPUT = { + CONF_LATITUDE: 55.55, + CONF_LONGITUDE: 42.42, + CONF_STATION: "XX/1234567", + CONF_LANGUAGE: "Gibberish", +} + + +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Environment Canada integration in Home Assistant.""" + + def mock_ec(): + ec_mock = MagicMock() + ec_mock.station_id = FIXTURE_USER_INPUT[CONF_STATION] + ec_mock.lat = FIXTURE_USER_INPUT[CONF_LATITUDE] + ec_mock.lon = FIXTURE_USER_INPUT[CONF_LONGITUDE] + ec_mock.language = FIXTURE_USER_INPUT[CONF_LANGUAGE] + ec_mock.update = AsyncMock() + return ec_mock + + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + ec_data = json.loads( + load_fixture("environment_canada/current_conditions_data.json") + ) + + weather_mock = mock_ec() + ec_data["metadata"]["timestamp"] = datetime(2022, 10, 4, tzinfo=timezone.utc) + weather_mock.conditions = ec_data["conditions"] + weather_mock.alerts = ec_data["alerts"] + weather_mock.daily_forecasts = ec_data["daily_forecasts"] + weather_mock.metadata = ec_data["metadata"] + + radar_mock = mock_ec() + radar_mock.image = b"GIF..." + radar_mock.timestamp = datetime(2022, 10, 4, tzinfo=timezone.utc) + + with patch( + "homeassistant.components.environment_canada.ECWeather", + return_value=weather_mock, + ), patch( + "homeassistant.components.environment_canada.ECAirQuality", + return_value=mock_ec(), + ), patch( + "homeassistant.components.environment_canada.ECRadar", return_value=radar_mock + ), patch( + "homeassistant.components.environment_canada.config_flow.ECWeather", + return_value=weather_mock, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def test_entry_diagnostics(hass, hass_client): + """Test config entry diagnostics.""" + + config_entry = await init_integration(hass) + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) + redacted_entry = json.loads( + load_fixture("environment_canada/config_entry_data.json") + ) + + assert diagnostics == redacted_entry diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index a4d1f416868..efeb2d376cf 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -559,6 +559,53 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): assert result["errors"] assert result["errors"]["base"] == "invalid_psk" + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + +async def test_reauth_confirm_invalid_with_unique_id(hass, mock_client, mock_zeroconf): + """Test reauth initiation with invalid PSK.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="test", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] + assert result["errors"]["base"] == "invalid_psk" + + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + async def test_discovery_dhcp_updates_host(hass, mock_client): """Test dhcp discovery updates host and aborts.""" diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 319bc2602e1..522ae0c8345 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -1,6 +1,7 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" from aiohttp import ClientSession +import pytest from homeassistant.components.esphome import CONF_NOISE_PSK from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT @@ -11,7 +12,10 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_diagnostics( - hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, + enable_bluetooth: pytest.fixture, ): """Test diagnostics for config entry.""" result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index f68bb5fe4ca..080cc3f4458 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.fibaro import DOMAIN +from homeassistant.components.fibaro.config_flow import _normalize_url from homeassistant.components.fibaro.const import CONF_IMPORT_PLUGINS from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -362,3 +363,9 @@ async def test_reauth_auth_failure(hass): assert result["type"] == "form" assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize("url_path", ["/api/", "/api", "/", ""]) +async def test_normalize_url(url_path: str) -> None: + """Test that the url is normalized for different entered values.""" + assert _normalize_url(f"http://192.168.1.1{url_path}") == "http://192.168.1.1/api/" diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index bcece50f6e4..13d8515f8f2 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -17,7 +17,6 @@ class FidoClientMock: def __init__(self, username, password, timeout=None, httpsession=None): """Fake Fido client init.""" - pass def get_phone_numbers(self): """Return Phone numbers.""" @@ -29,7 +28,6 @@ class FidoClientMock: async def fetch_data(self): """Return fake fetching data.""" - pass class FidoClientMockError(FidoClientMock): @@ -40,7 +38,7 @@ class FidoClientMockError(FidoClientMock): raise PyFidoError("Fake Error") -async def test_fido_sensor(loop, hass): +async def test_fido_sensor(event_loop, hass): """Test the Fido number sensor.""" with patch("homeassistant.components.fido.sensor.FidoClient", new=FidoClientMock): config = { diff --git a/tests/components/file_upload/conftest.py b/tests/components/file_upload/conftest.py new file mode 100644 index 00000000000..ab9965c1914 --- /dev/null +++ b/tests/components/file_upload/conftest.py @@ -0,0 +1,13 @@ +"""Fixtures for FileUpload integration.""" +from io import StringIO + +import pytest + + +@pytest.fixture +def large_file_io() -> StringIO: + """Generate a file on the fly. Simulates a large file.""" + return StringIO( + 2 + * "Home Assistant is awesome. Open source home automation that puts local control and privacy first." + ) diff --git a/tests/components/file_upload/test_init.py b/tests/components/file_upload/test_init.py index ba3485c96e1..699fb6f9b84 100644 --- a/tests/components/file_upload/test_init.py +++ b/tests/components/file_upload/test_init.py @@ -64,3 +64,49 @@ async def test_removed_on_stop(hass: HomeAssistant, hass_client, uploaded_file_d # Test it's removed assert not uploaded_file_dir.exists() + + +async def test_upload_large_file(hass: HomeAssistant, hass_client, large_file_io): + """Test uploading large file.""" + assert await async_setup_component(hass, "file_upload", {}) + client = await hass_client() + + with patch( + # Patch temp dir name to avoid tests fail running in parallel + "homeassistant.components.file_upload.TEMP_DIR_NAME", + file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", + ), patch( + # Patch one megabyte to 8 bytes to prevent having to use big files in tests + "homeassistant.components.file_upload.ONE_MEGABYTE", + 8, + ): + res = await client.post("/api/file_upload", data={"file": large_file_io}) + + assert res.status == 200 + response = await res.json() + + file_dir = hass.data[file_upload.DOMAIN].file_dir(response["file_id"]) + assert file_dir.is_dir() + + large_file_io.seek(0) + with file_upload.process_uploaded_file(hass, file_dir.name) as file_path: + assert file_path.is_file() + assert file_path.parent == file_dir + assert file_path.read_bytes() == large_file_io.read().encode("utf-8") + + +async def test_upload_with_wrong_key_fails( + hass: HomeAssistant, hass_client, large_file_io +): + """Test uploading fails.""" + assert await async_setup_component(hass, "file_upload", {}) + client = await hass_client() + + with patch( + # Patch temp dir name to avoid tests fail running in parallel + "homeassistant.components.file_upload.TEMP_DIR_NAME", + file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", + ): + res = await client.post("/api/file_upload", data={"wrong_key": large_file_io}) + + assert res.status == 400 diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 1b8a1928b1f..51fbf2941f8 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_EMAIL, CONF_PASSWORD, + PERCENTAGE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -29,6 +30,7 @@ MOCK_FLIPR_MEASURE = { "date_time": MOCK_DATE_TIME, "ph_status": "TooLow", "chlorine_status": "Medium", + "battery": 95.0, } @@ -94,6 +96,13 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "0.23654886" + state = hass.states.get("sensor.flipr_myfliprid_battery_level") + assert state + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.state == "95.0" + async def test_error_flipr_api_sensors(hass: HomeAssistant) -> None: """Test the Flipr sensors error.""" diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 31256c95866..ea6eb40b542 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator from datetime import datetime, timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from forecast_solar import models import pytest @@ -22,6 +22,15 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.forecast_solar.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 2380e65aabb..616dcba4a36 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Forecast.Solar config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_user_flow(hass: HomeAssistant) -> None: +async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test the full user configuration flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -27,20 +27,17 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert result.get("step_id") == SOURCE_USER assert "flow_id" in result - with patch( - "homeassistant.components.forecast_solar.async_setup_entry", return_value=True - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: "Name", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.42, - CONF_AZIMUTH: 142, - CONF_DECLINATION: 42, - CONF_MODULES_POWER: 4242, - }, - ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Name", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.42, + CONF_AZIMUTH: 142, + CONF_DECLINATION: 42, + CONF_MODULES_POWER: 4242, + }, + ) assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" @@ -57,16 +54,15 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_options_flow( - hass: HomeAssistant, mock_config_entry: MockConfigEntry +async def test_options_flow_invalid_api( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: - """Test config flow options.""" + """Test options config flow when API key is invalid.""" mock_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.forecast_solar.async_setup_entry", return_value=True - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) @@ -85,10 +81,85 @@ async def test_options_flow( CONF_INVERTER_SIZE: 2000, }, ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_options_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config flow options.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + # With the API key + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "SolarForecast150", + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, + }, + ) + await hass.async_block_till_done() assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { - CONF_API_KEY: "solarPOWER!", + CONF_API_KEY: "SolarForecast150", + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, + } + + +async def test_options_flow_without_key( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config flow options.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + # Without the API key + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("data") == { + CONF_API_KEY: None, CONF_DECLINATION: 21, CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index 957c52a88c5..1cb0260f058 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -379,7 +379,7 @@ async def test_async_browse_image(hass, hass_client, config_entry): (MediaType.ARTIST, "3815427709949443149"), (MediaType.TRACK, "456"), ): - mock_fetch_image.return_value = (b"image_bytes", media_type) + mock_fetch_image.return_value = (b"image_bytes", "image/jpeg") media_content_id = create_media_content_id( "title", media_type=media_type, id_or_path=media_id ) @@ -391,7 +391,7 @@ async def test_async_browse_image(hass, hass_client, config_entry): == f"http://owntone_instance/some_{media_type}_image" ) assert resp.status == HTTPStatus.OK - assert resp.content_type == media_type + assert resp.content_type == "image/jpeg" assert await resp.read() == b"image_bytes" diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index ae5e29bee47..9973bcd0d72 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -785,7 +785,7 @@ async def test_websocket_disconnect(hass, mock_api_object): assert hass.states.get(TEST_MASTER_ENTITY_NAME).state != STATE_UNAVAILABLE assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state != STATE_UNAVAILABLE updater_disconnected = mock_api_object.start_websocket_handler.call_args[0][4] - updater_disconnected() + await updater_disconnected() await hass.async_block_till_done() assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 32f2211d16b..fb62e14bc6f 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -138,6 +138,7 @@ MOCK_FB_SERVICES: dict[str, dict] = { "NewUptime": 35307, }, "GetExternalIPAddress": {"NewExternalIPAddress": "1.2.3.4"}, + "X_AVM_DE_GetExternalIPv6Address": {"NewExternalIPv6Address": "fec0::1"}, }, "WANPPPConnection1": { "GetInfo": { diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 2a3435210e7..117e9d31967 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -35,6 +35,10 @@ SENSOR_STATES: dict[str, dict[str, Any]] = { ATTR_STATE: "1.2.3.4", ATTR_ICON: "mdi:earth", }, + "sensor.mock_title_external_ipv6": { + ATTR_STATE: "fec0::1", + ATTR_ICON: "mdi:earth", + }, "sensor.mock_title_device_uptime": { # ATTR_STATE: "2022-02-05T17:46:04+00:00", ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index acafd19c924..fe7d11068fd 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -24,6 +24,7 @@ async def setup_config_entry( unique_id: str = "any", device: Mock = None, fritz: Mock = None, + template: Mock = None, ) -> bool: """Do setup of a MockConfigEntry.""" entry = MockConfigEntry( @@ -34,13 +35,17 @@ async def setup_config_entry( entry.add_to_hass(hass) if device is not None and fritz is not None: fritz().get_devices.return_value = [device] + + if template is not None and fritz is not None: + fritz().get_templates.return_value = [template] + result = await hass.config_entries.async_setup(entry.entry_id) if device is not None: await hass.async_block_till_done() return result -class FritzDeviceBaseMock(Mock): +class FritzEntityBaseMock(Mock): """base mock of a AVM Fritz!Box binary sensor device.""" ain = CONF_FAKE_AIN @@ -49,7 +54,7 @@ class FritzDeviceBaseMock(Mock): productname = CONF_FAKE_PRODUCTNAME -class FritzDeviceBinarySensorMock(FritzDeviceBaseMock): +class FritzDeviceBinarySensorMock(FritzEntityBaseMock): """Mock of a AVM Fritz!Box binary sensor device.""" alert_state = "fake_state" @@ -65,7 +70,7 @@ class FritzDeviceBinarySensorMock(FritzDeviceBaseMock): present = True -class FritzDeviceClimateMock(FritzDeviceBaseMock): +class FritzDeviceClimateMock(FritzEntityBaseMock): """Mock of a AVM Fritz!Box climate device.""" actual_temperature = 18.0 @@ -96,7 +101,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): scheduled_preset = PRESET_ECO -class FritzDeviceSensorMock(FritzDeviceBaseMock): +class FritzDeviceSensorMock(FritzEntityBaseMock): """Mock of a AVM Fritz!Box sensor device.""" battery_level = 23 @@ -115,7 +120,7 @@ class FritzDeviceSensorMock(FritzDeviceBaseMock): rel_humidity = 42 -class FritzDeviceSwitchMock(FritzDeviceBaseMock): +class FritzDeviceSwitchMock(FritzEntityBaseMock): """Mock of a AVM Fritz!Box switch device.""" battery_level = None @@ -137,7 +142,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock): temperature = 1.23 -class FritzDeviceLightMock(FritzDeviceBaseMock): +class FritzDeviceLightMock(FritzEntityBaseMock): """Mock of a AVM Fritz!Box light device.""" fw_version = "1.2.3" @@ -153,7 +158,7 @@ class FritzDeviceLightMock(FritzDeviceBaseMock): state = True -class FritzDeviceCoverMock(FritzDeviceBaseMock): +class FritzDeviceCoverMock(FritzEntityBaseMock): """Mock of a AVM Fritz!Box cover device.""" fw_version = "1.2.3" diff --git a/tests/components/fritzbox/test_button.py b/tests/components/fritzbox/test_button.py new file mode 100644 index 00000000000..b362e7dcfb1 --- /dev/null +++ b/tests/components/fritzbox/test_button.py @@ -0,0 +1,43 @@ +"""Tests for AVM Fritz!Box templates.""" +from unittest.mock import Mock + +from homeassistant.components.button import DOMAIN, SERVICE_PRESS +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + CONF_DEVICES, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant + +from . import FritzEntityBaseMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG + +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" + + +async def test_setup(hass: HomeAssistant, fritz: Mock): + """Test if is initialized correctly.""" + template = FritzEntityBaseMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], fritz=fritz, template=template + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME + assert state.state == STATE_UNKNOWN + + +async def test_apply_template(hass: HomeAssistant, fritz: Mock): + """Test if applies works.""" + template = FritzEntityBaseMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], fritz=fritz, template=template + ) + + assert await hass.services.async_call( + DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert fritz().apply_template.call_count == 1 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index fdf787d4cf2..d68d9e1679c 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -167,7 +167,9 @@ async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock) assert await hass.config_entries.async_setup(entry.entry_id) assert fritz().update_devices.call_count == 2 + assert fritz().update_templates.call_count == 1 assert fritz().get_devices.call_count == 1 + assert fritz().get_templates.call_count == 1 assert fritz().login.call_count == 2 @@ -187,6 +189,7 @@ async def test_coordinator_update_after_password_change( assert not await hass.config_entries.async_setup(entry.entry_id) assert fritz().update_devices.call_count == 1 assert fritz().get_devices.call_count == 0 + assert fritz().get_templates.call_count == 0 assert fritz().login.call_count == 2 diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 661b3ace38a..6bff327d397 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -80,7 +80,7 @@ async def frontend_themes(hass): @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 9959023e8a6..9aaa356085b 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -117,7 +117,7 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture -async def geofency_client(loop, hass, hass_client_no_auth): +async def geofency_client(event_loop, hass, hass_client_no_auth): """Geofency mock client (unauthenticated).""" assert await async_setup_component( @@ -130,7 +130,7 @@ async def geofency_client(loop, hass, hass_client_no_auth): @pytest.fixture(autouse=True) -async def setup_zones(loop, hass): +async def setup_zones(event_loop, hass): """Set up Zone config in HA.""" assert await async_setup_component( hass, diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index 488265f970b..4818e9258de 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -1 +1,42 @@ """Tests for Glances.""" + +MOCK_USER_INPUT = { + "host": "0.0.0.0", + "username": "username", + "password": "password", + "version": 3, + "port": 61208, + "ssl": False, + "verify_ssl": True, +} + +MOCK_DATA = { + "cpu": { + "total": 10.6, + "user": 7.6, + "system": 2.1, + "idle": 88.8, + "nice": 0.0, + "iowait": 0.6, + }, + "diskio": [ + { + "time_since_update": 1, + "disk_name": "nvme0n1", + "read_count": 12, + "write_count": 466, + "read_bytes": 184320, + "write_bytes": 23863296, + "key": "disk_name", + }, + ], + "system": { + "os_name": "Linux", + "hostname": "fedora-35", + "platform": "64bit", + "linux_distro": "Fedora Linux 35", + "os_version": "5.15.6-200.fc35.x86_64", + "hr_name": "Fedora Linux 35 64bit", + }, + "uptime": "3 days, 10:25:20", +} diff --git a/tests/components/glances/conftest.py b/tests/components/glances/conftest.py new file mode 100644 index 00000000000..d92d3cc33d4 --- /dev/null +++ b/tests/components/glances/conftest.py @@ -0,0 +1,15 @@ +"""Conftest for speedtestdotnet.""" +from unittest.mock import AsyncMock, patch + +import pytest + +from . import MOCK_DATA + + +@pytest.fixture(autouse=True) +def mock_api(): + """Mock glances api.""" + with patch("homeassistant.components.glances.Glances") as mock_api: + mock_api.return_value.get_data = AsyncMock(return_value=None) + mock_api.return_value.data.return_value = MOCK_DATA + yield mock_api diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 40e40b45e11..ab642055059 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -1,38 +1,22 @@ """Tests for Glances config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock -from glances_api import exceptions +from glances_api.exceptions import GlancesApiConnectionError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import glances -from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from . import MOCK_USER_INPUT -NAME = "Glances" -HOST = "0.0.0.0" -USERNAME = "username" -PASSWORD = "password" -PORT = 61208 -VERSION = 3 -SCAN_INTERVAL = 10 - -DEMO_USER_INPUT = { - "host": HOST, - "username": USERNAME, - "password": PASSWORD, - "version": VERSION, - "port": PORT, - "ssl": False, - "verify_ssl": True, -} +from tests.common import MockConfigEntry, patch @pytest.fixture(autouse=True) def glances_setup_fixture(): - """Mock transmission entry setup.""" + """Mock glances entry setup.""" with patch("homeassistant.components.glances.async_setup_entry", return_value=True): yield @@ -43,74 +27,43 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("homeassistant.components.glances.Glances.get_data", autospec=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_INPUT + ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_USER_INPUT - ) - - assert result["type"] == "create_entry" - assert result["title"] == HOST - assert result["data"] == DEMO_USER_INPUT + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "0.0.0.0" + assert result["data"] == MOCK_USER_INPUT -async def test_form_cannot_connect(hass: HomeAssistant) -> None: +async def test_form_cannot_connect(hass: HomeAssistant, mock_api: MagicMock) -> None: """Test to return error if we cannot connect.""" - with patch( - "homeassistant.components.glances.Glances.get_data", - side_effect=exceptions.GlancesApiConnectionError, - ): - result = await hass.config_entries.flow.async_init( - glances.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_USER_INPUT - ) + mock_api.return_value.get_data.side_effect = GlancesApiConnectionError + result = await hass.config_entries.flow.async_init( + glances.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_INPUT + ) - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} async def test_form_already_configured(hass: HomeAssistant) -> None: """Test host is already configured.""" - entry = MockConfigEntry( - domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} - ) + entry = MockConfigEntry(domain=glances.DOMAIN, data=MOCK_USER_INPUT) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_USER_INPUT + result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_options(hass: HomeAssistant) -> None: - """Test options for Glances.""" - entry = MockConfigEntry( - domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} - ) - 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"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={glances.CONF_SCAN_INTERVAL: 10} - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { - glances.CONF_SCAN_INTERVAL: 10, - } diff --git a/tests/components/glances/test_init.py b/tests/components/glances/test_init.py new file mode 100644 index 00000000000..944d9d55ae2 --- /dev/null +++ b/tests/components/glances/test_init.py @@ -0,0 +1,49 @@ +"""Tests for Glances integration.""" +from unittest.mock import MagicMock + +from glances_api.exceptions import GlancesApiConnectionError + +from homeassistant.components.glances.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_USER_INPUT + +from tests.common import MockConfigEntry + + +async def test_successful_config_entry(hass: HomeAssistant) -> None: + """Test that Glances is configured successfully.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED + + +async def test_conn_error(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test Glances failed due to connection error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + mock_api.return_value.get_data.side_effect = GlancesApiConnectionError + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test removing Glances.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert DOMAIN not in hass.data diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index e61015a4925..402c03f2e51 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -77,7 +77,7 @@ async def test_sensors( assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_time_to_empty_full") assert state.state == "-1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == TIME_MINUTES + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DURATION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_temperature") diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index c813bd55782..0e53642548d 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -70,15 +70,11 @@ def calendar_access_role(request) -> str: @pytest.fixture(autouse=True) def mock_test_setup( - hass, test_api_calendar, mock_calendars_list, - config_entry, ): - """Fixture that pulls in the default fixtures for tests in this file.""" + """Fixture that sets up the default API responses during integration setup.""" mock_calendars_list({"items": [test_api_calendar]}) - config_entry.add_to_hass(hass) - return def get_events_url(entity: str, start: str, end: str) -> str: @@ -313,15 +309,12 @@ async def test_missing_summary(hass, mock_events_list_items, component_setup): async def test_update_error( hass, component_setup, - mock_calendars_list, mock_events_list, - test_api_calendar, aioclient_mock, ): """Test that the calendar update handles a server error.""" now = dt_util.now() - mock_calendars_list({"items": [test_api_calendar]}) mock_events_list( { "items": [ @@ -528,7 +521,6 @@ async def test_opaque_event( async def test_scan_calendar_error( hass, component_setup, - test_api_calendar, mock_calendars_list, config_entry, ): diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 3fd35846d7e..97484f31341 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -42,8 +42,9 @@ def auth_header(hass_access_token): @pytest.fixture -def assistant_client(loop, hass, hass_client_no_auth): +def assistant_client(event_loop, hass, hass_client_no_auth): """Create web client for the Google Assistant API.""" + loop = event_loop loop.run_until_complete( setup.async_setup_component( hass, @@ -66,8 +67,10 @@ def assistant_client(loop, hass, hass_client_no_auth): @pytest.fixture -def hass_fixture(loop, hass): +def hass_fixture(event_loop, hass): """Set up a Home Assistant instance for these tests.""" + loop = event_loop + # We need to do this to get access to homeassistant/turn_(on,off) loop.run_until_complete(setup.async_setup_component(hass, core.DOMAIN, {})) diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 4d246cff589..77357065a1a 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -26,7 +26,7 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture -async def gpslogger_client(loop, hass, hass_client_no_auth): +async def gpslogger_client(event_loop, hass, hass_client_no_auth): """Mock client for GPSLogger (unauthenticated).""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -38,7 +38,7 @@ async def gpslogger_client(loop, hass, hass_client_no_auth): @pytest.fixture(autouse=True) -async def setup_zones(loop, hass): +async def setup_zones(event_loop, hass): """Set up Zone config in HA.""" assert await async_setup_component( hass, diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 42bc96dbc6e..4549a7f5fec 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -1,5 +1,5 @@ """The tests for the Media group platform.""" -from unittest.mock import patch +from unittest.mock import Mock, patch import async_timeout import pytest @@ -43,6 +43,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -57,7 +58,7 @@ def media_player_media_seek_fixture(): yield seek -async def test_default_state(hass): +async def test_default_state(hass: HomeAssistant) -> None: """Test media group default state.""" hass.states.async_set("media_player.player_1", "on") await async_setup_component( @@ -91,7 +92,7 @@ async def test_default_state(hass): assert entry.unique_id == "unique_identifier" -async def test_state_reporting(hass): +async def test_state_reporting(hass: HomeAssistant) -> None: """Test the state reporting. The group state is unavailable if all group members are unavailable. @@ -170,6 +171,12 @@ async def test_state_reporting(hass): await hass.async_block_till_done() assert hass.states.get("media_player.media_group").state == STATE_OFF + # All group members in same invalid state -> unknown + hass.states.async_set("media_player.player_1", "invalid_state") + hass.states.async_set("media_player.player_2", "invalid_state") + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN + # All group members removed from the state machine -> unavailable hass.states.async_remove("media_player.player_1") hass.states.async_remove("media_player.player_2") @@ -177,7 +184,7 @@ async def test_state_reporting(hass): assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE -async def test_supported_features(hass): +async def test_supported_features(hass: HomeAssistant) -> None: """Test supported features reporting.""" pause_play_stop = ( MediaPlayerEntityFeature.PAUSE @@ -241,7 +248,7 @@ async def test_supported_features(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == pause_play_stop | play_media -async def test_service_calls(hass, mock_media_seek): +async def test_service_calls(hass: HomeAssistant, mock_media_seek: Mock) -> None: """Test service calls.""" await async_setup_component( hass, @@ -533,7 +540,7 @@ async def test_service_calls(hass, mock_media_seek): assert hass.states.get("media_player.living_room").state == STATE_OFF -async def test_nested_group(hass): +async def test_nested_group(hass: HomeAssistant) -> None: """Test nested media group.""" await async_setup_component( hass, diff --git a/tests/components/hangouts/__init__.py b/tests/components/hangouts/__init__.py deleted file mode 100644 index 81174356c2e..00000000000 --- a/tests/components/hangouts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Hangouts Component.""" diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py deleted file mode 100644 index 5df675a0f05..00000000000 --- a/tests/components/hangouts/test_config_flow.py +++ /dev/null @@ -1,132 +0,0 @@ -"""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 - -EMAIL = "test@test.com" -PASSWORD = "1232456" - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow without 2fa.""" - flow = config_flow.HangoutsFlowHandler() - - flow.hass = hass - - with patch("homeassistant.components.hangouts.config_flow.get_auth"): - result = await flow.async_step_user( - {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == EMAIL - - -async def test_flow_works_with_authcode(hass, aioclient_mock): - """Test config flow without 2fa.""" - flow = config_flow.HangoutsFlowHandler() - - flow.hass = hass - - with patch("homeassistant.components.hangouts.config_flow.get_auth"): - result = await flow.async_step_user( - { - CONF_EMAIL: EMAIL, - CONF_PASSWORD: PASSWORD, - "authorization_code": "c29tZXJhbmRvbXN0cmluZw==", - } - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == EMAIL - - -async def test_flow_works_with_2fa(hass, aioclient_mock): - """Test config flow with 2fa.""" - from homeassistant.components.hangouts.hangups_utils import Google2FAError - - flow = config_flow.HangoutsFlowHandler() - - flow.hass = hass - - with patch( - "homeassistant.components.hangouts.config_flow.get_auth", - side_effect=Google2FAError, - ): - result = await flow.async_step_user( - {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "2fa" - - with patch("homeassistant.components.hangouts.config_flow.get_auth"): - result = await flow.async_step_2fa({"2fa": 123456}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == EMAIL - - -async def test_flow_with_unknown_2fa(hass, aioclient_mock): - """Test config flow with invalid 2fa method.""" - from homeassistant.components.hangouts.hangups_utils import GoogleAuthError - - flow = config_flow.HangoutsFlowHandler() - - flow.hass = hass - - with patch( - "homeassistant.components.hangouts.config_flow.get_auth", - side_effect=GoogleAuthError("Unknown verification code input"), - ): - result = await flow.async_step_user( - {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"]["base"] == "invalid_2fa_method" - - -async def test_flow_invalid_login(hass, aioclient_mock): - """Test config flow with invalid 2fa method.""" - from homeassistant.components.hangouts.hangups_utils import GoogleAuthError - - flow = config_flow.HangoutsFlowHandler() - - flow.hass = hass - - with patch( - "homeassistant.components.hangouts.config_flow.get_auth", - side_effect=GoogleAuthError, - ): - result = await flow.async_step_user( - {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"]["base"] == "invalid_login" - - -async def test_flow_invalid_2fa(hass, aioclient_mock): - """Test config flow with 2fa.""" - from homeassistant.components.hangouts.hangups_utils import Google2FAError - - flow = config_flow.HangoutsFlowHandler() - - flow.hass = hass - - with patch( - "homeassistant.components.hangouts.config_flow.get_auth", - side_effect=Google2FAError, - ): - result = await flow.async_step_user( - {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "2fa" - - with patch( - "homeassistant.components.hangouts.config_flow.get_auth", - side_effect=Google2FAError, - ): - result = await flow.async_step_2fa({"2fa": 123456}) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"]["base"] == "invalid_2fa" diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py index 33602f92e3f..e35c94e4926 100644 --- a/tests/components/hardkernel/test_hardware.py +++ b/tests/components/hardkernel/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "odroid-n2", "revision": None, }, + "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Blue / Hardkernel Odroid-N2", "url": None, diff --git a/tests/components/harmony/test_select.py b/tests/components/harmony/test_select.py index 4607f035893..60bb85ed0c3 100644 --- a/tests/components/harmony/test_select.py +++ b/tests/components/harmony/test_select.py @@ -9,6 +9,7 @@ from homeassistant.components.select import ( SERVICE_SELECT_OPTION, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, @@ -68,11 +69,12 @@ async def test_options(mock_hc, hass, mock_write_config): # assert we have all options state = hass.states.get(ENTITY_SELECT) assert state.attributes.get("options") == [ - "PowerOff", + "power_off", "Nile-TV", "Play Music", "Watch TV", ] + assert state.attributes.get(ATTR_DEVICE_CLASS) == "harmony__activities" async def test_select_option(mock_hc, hass, mock_write_config): @@ -94,10 +96,10 @@ async def test_select_option(mock_hc, hass, mock_write_config): assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) assert hass.states.is_state(ENTITY_SELECT, "Play Music") - # turn off harmony by selecting PowerOff activity - await _select_option_and_wait(hass, ENTITY_SELECT, "PowerOff") + # turn off harmony by selecting power_off activity + await _select_option_and_wait(hass, ENTITY_SELECT, "power_off") assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) - assert hass.states.is_state(ENTITY_SELECT, "PowerOff") + assert hass.states.is_state(ENTITY_SELECT, "power_off") async def _select_option_and_wait(hass, entity, option): diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py new file mode 100644 index 00000000000..7135f1ea646 --- /dev/null +++ b/tests/components/hassio/test_addon_manager.py @@ -0,0 +1,1134 @@ +"""Test the addon manager.""" +from __future__ import annotations + +import asyncio +from collections.abc import Generator +import logging +from typing import Any +from unittest.mock import AsyncMock, call, patch + +import pytest + +from homeassistant.components.hassio.addon_manager import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, +) +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.core import HomeAssistant + +LOGGER = logging.getLogger(__name__) + + +@pytest.fixture(name="addon_manager") +def addon_manager_fixture(hass: HomeAssistant) -> AddonManager: + """Return an AddonManager instance.""" + return AddonManager(hass, LOGGER, "Test", "test_addon") + + +@pytest.fixture(name="addon_not_installed") +def addon_not_installed_fixture( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> AsyncMock: + """Mock add-on not installed.""" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> AsyncMock: + """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-test-addon" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="get_addon_discovery_info") +def get_addon_discovery_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock get add-on discovery info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info" + ) as get_addon_discovery_info: + yield get_addon_discovery_info + + +@pytest.fixture(name="addon_store_info") +def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock Supervisor add-on store info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + +@pytest.fixture(name="addon_info") +def addon_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_info", + ) as addon_info: + addon_info.return_value = { + "hostname": None, + "options": {}, + "state": None, + "update_available": False, + "version": None, + } + yield addon_info + + +@pytest.fixture(name="set_addon_options") +def set_addon_options_fixture() -> Generator[AsyncMock, None, None]: + """Mock set add-on options.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_set_addon_options" + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon") +def install_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock install add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_install_addon" + ) as install_addon: + yield install_addon + + +@pytest.fixture(name="uninstall_addon") +def uninstall_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon + + +@pytest.fixture(name="start_addon") +def start_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock start add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_start_addon" + ) as start_addon: + yield start_addon + + +@pytest.fixture(name="restart_addon") +def restart_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock restart add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_restart_addon" + ) as restart_addon: + yield restart_addon + + +@pytest.fixture(name="stop_addon") +def stop_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock stop add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_stop_addon" + ) as stop_addon: + yield stop_addon + + +@pytest.fixture(name="create_backup") +def create_backup_fixture() -> Generator[AsyncMock, None, None]: + """Mock create backup.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_create_backup" + ) as create_backup: + yield create_backup + + +@pytest.fixture(name="update_addon") +def mock_update_addon() -> Generator[AsyncMock, None, None]: + """Mock update add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_update_addon" + ) as update_addon: + yield update_addon + + +async def test_not_installed_raises_exception( + addon_manager: AddonManager, + addon_not_installed: dict[str, Any], +) -> None: + """Test addon not installed raises exception.""" + addon_config = {"test_key": "test"} + + with pytest.raises(AddonError) as err: + await addon_manager.async_configure_addon(addon_config) + + assert str(err.value) == "Test add-on is not installed" + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Test add-on is not installed" + + +async def test_get_addon_discovery_info( + addon_manager: AddonManager, get_addon_discovery_info: AsyncMock +) -> None: + """Test get addon discovery info.""" + get_addon_discovery_info.return_value = {"config": {"test_key": "test"}} + + assert await addon_manager.async_get_addon_discovery_info() == {"test_key": "test"} + + assert get_addon_discovery_info.call_count == 1 + + +async def test_missing_addon_discovery_info( + addon_manager: AddonManager, get_addon_discovery_info: AsyncMock +) -> None: + """Test missing addon discovery info.""" + get_addon_discovery_info.return_value = None + + with pytest.raises(AddonError): + await addon_manager.async_get_addon_discovery_info() + + assert get_addon_discovery_info.call_count == 1 + + +async def test_get_addon_discovery_info_error( + addon_manager: AddonManager, get_addon_discovery_info: AsyncMock +) -> None: + """Test get addon discovery info raises error.""" + get_addon_discovery_info.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + assert await addon_manager.async_get_addon_discovery_info() + + assert str(err.value) == "Failed to get the Test add-on discovery info: Boom" + + assert get_addon_discovery_info.call_count == 1 + + +async def test_get_addon_info_not_installed( + addon_manager: AddonManager, addon_not_installed: AsyncMock +) -> None: + """Test get addon info when addon is not installed..""" + assert await addon_manager.async_get_addon_info() == AddonInfo( + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + +@pytest.mark.parametrize( + "addon_info_state, addon_state", + [("started", AddonState.RUNNING), ("stopped", AddonState.NOT_RUNNING)], +) +async def test_get_addon_info( + addon_manager: AddonManager, + addon_installed: AsyncMock, + addon_info_state: str, + addon_state: AddonState, +) -> None: + """Test get addon info when addon is installed.""" + addon_installed.return_value["state"] = addon_info_state + assert await addon_manager.async_get_addon_info() == AddonInfo( + hostname="core-test-addon", + options={}, + state=addon_state, + update_available=False, + version="1.0.0", + ) + + +@pytest.mark.parametrize( + "addon_info_error, addon_info_calls, addon_store_info_error, addon_store_info_calls", + [(HassioAPIError("Boom"), 1, None, 1), (None, 0, HassioAPIError("Boom"), 1)], +) +async def test_get_addon_info_error( + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + addon_installed: AsyncMock, + addon_info_error: Exception | None, + addon_info_calls: int, + addon_store_info_error: Exception | None, + addon_store_info_calls: int, +) -> None: + """Test get addon info raises error.""" + addon_info.side_effect = addon_info_error + addon_store_info.side_effect = addon_store_info_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_get_addon_info() + + assert str(err.value) == "Failed to get the Test add-on info: Boom" + + assert addon_info.call_count == addon_info_calls + assert addon_store_info.call_count == addon_store_info_calls + + +async def test_set_addon_options( + hass: HomeAssistant, addon_manager: AddonManager, set_addon_options: AsyncMock +) -> None: + """Test set addon options.""" + await addon_manager.async_set_addon_options({"test_key": "test"}) + + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + hass, "test_addon", {"options": {"test_key": "test"}} + ) + + +async def test_set_addon_options_error( + hass: HomeAssistant, addon_manager: AddonManager, set_addon_options: AsyncMock +) -> None: + """Test set addon options raises error.""" + set_addon_options.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_set_addon_options({"test_key": "test"}) + + assert str(err.value) == "Failed to set the Test add-on options: Boom" + + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + hass, "test_addon", {"options": {"test_key": "test"}} + ) + + +async def test_install_addon( + addon_manager: AddonManager, install_addon: AsyncMock +) -> None: + """Test install addon.""" + await addon_manager.async_install_addon() + + assert install_addon.call_count == 1 + + +async def test_install_addon_error( + addon_manager: AddonManager, install_addon: AsyncMock +) -> None: + """Test install addon raises error.""" + install_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_install_addon() + + assert str(err.value) == "Failed to install the Test add-on: Boom" + + assert install_addon.call_count == 1 + + +async def test_schedule_install_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, +) -> None: + """Test schedule install addon.""" + install_task = addon_manager.async_schedule_install_addon() + + assert addon_manager.task_in_progress() is True + + assert await addon_manager.async_get_addon_info() == AddonInfo( + hostname="core-test-addon", + options={}, + state=AddonState.INSTALLING, + update_available=False, + version="1.0.0", + ) + + # Make sure that actually only one install task is running. + install_task_two = addon_manager.async_schedule_install_addon() + + await asyncio.gather(install_task, install_task_two) + + assert addon_manager.task_in_progress() is False + assert install_addon.call_count == 1 + + install_addon.reset_mock() + + # Test that another call can be made after the install is done. + await addon_manager.async_schedule_install_addon() + + assert install_addon.call_count == 1 + + +async def test_schedule_install_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, +) -> None: + """Test schedule install addon raises error.""" + install_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_install_addon() + + assert str(err.value) == "Failed to install the Test add-on: Boom" + + assert install_addon.call_count == 1 + + +async def test_schedule_install_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule install addon logs error.""" + install_addon.side_effect = HassioAPIError("Boom") + + await addon_manager.async_schedule_install_addon(catch_error=True) + + assert "Failed to install the Test add-on: Boom" in caplog.text + assert install_addon.call_count == 1 + + +async def test_uninstall_addon( + addon_manager: AddonManager, uninstall_addon: AsyncMock +) -> None: + """Test uninstall addon.""" + await addon_manager.async_uninstall_addon() + + assert uninstall_addon.call_count == 1 + + +async def test_uninstall_addon_error( + addon_manager: AddonManager, uninstall_addon: AsyncMock +) -> None: + """Test uninstall addon raises error.""" + uninstall_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_uninstall_addon() + + assert str(err.value) == "Failed to uninstall the Test add-on: Boom" + + assert uninstall_addon.call_count == 1 + + +async def test_start_addon(addon_manager: AddonManager, start_addon: AsyncMock) -> None: + """Test start addon.""" + await addon_manager.async_start_addon() + + assert start_addon.call_count == 1 + + +async def test_start_addon_error( + addon_manager: AddonManager, start_addon: AsyncMock +) -> None: + """Test start addon raises error.""" + start_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_start_addon() + + assert str(err.value) == "Failed to start the Test add-on: Boom" + + assert start_addon.call_count == 1 + + +async def test_schedule_start_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule start addon.""" + start_task = addon_manager.async_schedule_start_addon() + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one start task is running. + start_task_two = addon_manager.async_schedule_start_addon() + + await asyncio.gather(start_task, start_task_two) + + assert addon_manager.task_in_progress() is False + assert start_addon.call_count == 1 + + start_addon.reset_mock() + + # Test that another call can be made after the start is done. + await addon_manager.async_schedule_start_addon() + + assert start_addon.call_count == 1 + + +async def test_schedule_start_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule start addon raises error.""" + start_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_start_addon() + + assert str(err.value) == "Failed to start the Test add-on: Boom" + + assert start_addon.call_count == 1 + + +async def test_schedule_start_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + start_addon: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule start addon logs error.""" + start_addon.side_effect = HassioAPIError("Boom") + + await addon_manager.async_schedule_start_addon(catch_error=True) + + assert "Failed to start the Test add-on: Boom" in caplog.text + assert start_addon.call_count == 1 + + +async def test_restart_addon( + addon_manager: AddonManager, restart_addon: AsyncMock +) -> None: + """Test restart addon.""" + await addon_manager.async_restart_addon() + + assert restart_addon.call_count == 1 + + +async def test_restart_addon_error( + addon_manager: AddonManager, restart_addon: AsyncMock +) -> None: + """Test restart addon raises error.""" + restart_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_restart_addon() + + assert str(err.value) == "Failed to restart the Test add-on: Boom" + + assert restart_addon.call_count == 1 + + +async def test_schedule_restart_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + restart_addon: AsyncMock, +) -> None: + """Test schedule restart addon.""" + restart_task = addon_manager.async_schedule_restart_addon() + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one start task is running. + restart_task_two = addon_manager.async_schedule_restart_addon() + + await asyncio.gather(restart_task, restart_task_two) + + assert addon_manager.task_in_progress() is False + assert restart_addon.call_count == 1 + + restart_addon.reset_mock() + + # Test that another call can be made after the restart is done. + await addon_manager.async_schedule_restart_addon() + + assert restart_addon.call_count == 1 + + +async def test_schedule_restart_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + restart_addon: AsyncMock, +) -> None: + """Test schedule restart addon raises error.""" + restart_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_restart_addon() + + assert str(err.value) == "Failed to restart the Test add-on: Boom" + + assert restart_addon.call_count == 1 + + +async def test_schedule_restart_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + restart_addon: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule restart addon logs error.""" + restart_addon.side_effect = HassioAPIError("Boom") + + await addon_manager.async_schedule_restart_addon(catch_error=True) + + assert "Failed to restart the Test add-on: Boom" in caplog.text + assert restart_addon.call_count == 1 + + +async def test_stop_addon(addon_manager: AddonManager, stop_addon: AsyncMock) -> None: + """Test stop addon.""" + await addon_manager.async_stop_addon() + + assert stop_addon.call_count == 1 + + +async def test_stop_addon_error( + addon_manager: AddonManager, stop_addon: AsyncMock +) -> None: + """Test stop addon raises error.""" + stop_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_stop_addon() + + assert str(err.value) == "Failed to stop the Test add-on: Boom" + + assert stop_addon.call_count == 1 + + +async def test_update_addon( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test update addon.""" + addon_info.return_value["update_available"] = True + + await addon_manager.async_update_addon() + + assert addon_info.call_count == 2 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + assert update_addon.call_count == 1 + + +async def test_update_addon_no_update( + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test update addon without update available.""" + addon_info.return_value["update_available"] = False + + await addon_manager.async_update_addon() + + assert addon_info.call_count == 1 + assert create_backup.call_count == 0 + assert update_addon.call_count == 0 + + +async def test_update_addon_error( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test update addon raises error.""" + addon_info.return_value["update_available"] = True + update_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Failed to update the Test add-on: Boom" + + assert addon_info.call_count == 2 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + assert update_addon.call_count == 1 + + +async def test_schedule_update_addon( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test schedule update addon.""" + addon_info.return_value["update_available"] = True + + update_task = addon_manager.async_schedule_update_addon() + + assert addon_manager.task_in_progress() is True + + assert await addon_manager.async_get_addon_info() == AddonInfo( + hostname="core-test-addon", + options={}, + state=AddonState.UPDATING, + update_available=True, + version="1.0.0", + ) + + # Make sure that actually only one update task is running. + update_task_two = addon_manager.async_schedule_update_addon() + + await asyncio.gather(update_task, update_task_two) + + assert addon_manager.task_in_progress() is False + assert addon_info.call_count == 3 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + assert update_addon.call_count == 1 + + update_addon.reset_mock() + + # Test that another call can be made after the update is done. + await addon_manager.async_schedule_update_addon() + + assert update_addon.call_count == 1 + + +@pytest.mark.parametrize( + ( + "create_backup_error, create_backup_calls, " + "update_addon_error, update_addon_calls, " + "error_message" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to create a backup of the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to update the Test add-on: Boom", + ), + ], +) +async def test_schedule_update_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, + create_backup_error: Exception | None, + create_backup_calls: int, + update_addon_error: Exception | None, + update_addon_calls: int, + error_message: str, +) -> None: + """Test schedule update addon raises error.""" + addon_installed.return_value["update_available"] = True + create_backup.side_effect = create_backup_error + update_addon.side_effect = update_addon_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_update_addon() + + assert str(err.value) == error_message + + assert create_backup.call_count == create_backup_calls + assert update_addon.call_count == update_addon_calls + + +@pytest.mark.parametrize( + ( + "create_backup_error, create_backup_calls, " + "update_addon_error, update_addon_calls, " + "error_log" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to create a backup of the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to update the Test add-on: Boom", + ), + ], +) +async def test_schedule_update_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, + create_backup_error: Exception | None, + create_backup_calls: int, + update_addon_error: Exception | None, + update_addon_calls: int, + error_log: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule update addon logs error.""" + addon_installed.return_value["update_available"] = True + create_backup.side_effect = create_backup_error + update_addon.side_effect = update_addon_error + + await addon_manager.async_schedule_update_addon(catch_error=True) + + assert error_log in caplog.text + assert create_backup.call_count == create_backup_calls + assert update_addon.call_count == update_addon_calls + + +async def test_create_backup( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, +) -> None: + """Test creating a backup of the addon.""" + await addon_manager.async_create_backup() + + assert addon_info.call_count == 1 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + + +async def test_create_backup_error( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, +) -> None: + """Test creating a backup of the addon raises error.""" + create_backup.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_create_backup() + + assert str(err.value) == "Failed to create a backup of the Test add-on: Boom" + + assert addon_info.call_count == 1 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + + +async def test_schedule_install_setup_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule install setup addon.""" + install_task = addon_manager.async_schedule_install_setup_addon( + {"test_key": "test"} + ) + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one install task is running. + install_task_two = addon_manager.async_schedule_install_setup_addon( + {"test_key": "test"} + ) + + await asyncio.gather(install_task, install_task_two) + + assert addon_manager.task_in_progress() is False + assert install_addon.call_count == 1 + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + install_addon.reset_mock() + set_addon_options.reset_mock() + start_addon.reset_mock() + + # Test that another call can be made after the install is done. + await addon_manager.async_schedule_install_setup_addon({"test_key": "test"}) + + assert install_addon.call_count == 1 + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + +@pytest.mark.parametrize( + ( + "install_addon_error, install_addon_calls, " + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_message" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + None, + 0, + "Failed to install the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_install_setup_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + install_addon_error: Exception | None, + install_addon_calls: int, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_message: str, +) -> None: + """Test schedule install setup addon raises error.""" + install_addon.side_effect = install_addon_error + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_install_setup_addon({"test_key": "test"}) + + assert str(err.value) == error_message + + assert install_addon.call_count == install_addon_calls + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls + + +@pytest.mark.parametrize( + ( + "install_addon_error, install_addon_calls, " + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_log" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + None, + 0, + "Failed to install the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_install_setup_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + install_addon_error: Exception | None, + install_addon_calls: int, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_log: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule install setup addon logs error.""" + install_addon.side_effect = install_addon_error + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + await addon_manager.async_schedule_install_setup_addon( + {"test_key": "test"}, catch_error=True + ) + + assert error_log in caplog.text + assert install_addon.call_count == install_addon_calls + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls + + +async def test_schedule_setup_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule setup addon.""" + start_task = addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one start task is running. + start_task_two = addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + await asyncio.gather(start_task, start_task_two) + + assert addon_manager.task_in_progress() is False + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + set_addon_options.reset_mock() + start_addon.reset_mock() + + # Test that another call can be made after the start is done. + await addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + +@pytest.mark.parametrize( + ( + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_message" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_setup_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_message: str, +) -> None: + """Test schedule setup addon raises error.""" + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + assert str(err.value) == error_message + + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls + + +@pytest.mark.parametrize( + ( + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_log" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_setup_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_log: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule setup addon logs error.""" + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + await addon_manager.async_schedule_setup_addon( + {"test_key": "test"}, catch_error=True + ) + + assert error_log in caplog.text + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py index 368b070428e..8069583df76 100644 --- a/tests/components/here_travel_time/conftest.py +++ b/tests/components/here_travel_time/conftest.py @@ -2,37 +2,39 @@ import json from unittest.mock import patch -from herepy.models import RoutingResponse import pytest from tests.common import load_fixture -RESPONSE = RoutingResponse.new_from_jsondict( - json.loads(load_fixture("here_travel_time/car_response.json")) +RESPONSE = json.loads(load_fixture("here_travel_time/car_response.json")) +TRANSIT_RESPONSE = json.loads( + load_fixture("here_travel_time/transit_route_response.json") ) -RESPONSE.route_short = "US-29 - K St NW; US-29 - Whitehurst Fwy; I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" - -EMPTY_ATTRIBUTION_RESPONSE = RoutingResponse.new_from_jsondict( - json.loads(load_fixture("here_travel_time/empty_attribution_response.json")) +NO_ATTRIBUTION_TRANSIT_RESPONSE = json.loads( + load_fixture("here_travel_time/no_attribution_transit_route_response.json") ) -EMPTY_ATTRIBUTION_RESPONSE.route_short = "US-29 - K St NW; US-29 - Whitehurst Fwy; I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" @pytest.fixture(name="valid_response") def valid_response_fixture(): """Return valid api response.""" with patch( - "herepy.RoutingApi.public_transport_timetable", + "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE + ), patch( + "here_routing.HERERoutingApi.route", return_value=RESPONSE, ) as mock: yield mock -@pytest.fixture(name="empty_attribution_response") -def empty_attribution_response_fixture(): - """Return valid api response with an empty attribution.""" +@pytest.fixture(name="no_attribution_response") +def no_attribution_response_fixture(): + """Return valid api response without attribution.""" with patch( - "herepy.RoutingApi.public_transport_timetable", - return_value=EMPTY_ATTRIBUTION_RESPONSE, + "here_transit.HERETransitApi.route", + return_value=NO_ATTRIBUTION_TRANSIT_RESPONSE, + ), patch( + "here_routing.HERERoutingApi.route", + return_value=RESPONSE, ) as mock: yield mock diff --git a/tests/components/here_travel_time/const.py b/tests/components/here_travel_time/const.py index 0cc3143bc0b..167fd51dc5b 100644 --- a/tests/components/here_travel_time/const.py +++ b/tests/components/here_travel_time/const.py @@ -1,8 +1,27 @@ """Constants for HERE Travel Time tests.""" +from homeassistant.components.here_travel_time.const import ( + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + TRAVEL_MODE_CAR, +) +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME + API_KEY = "test" -CAR_ORIGIN_LATITUDE = "38.9" -CAR_ORIGIN_LONGITUDE = "-77.04833" -CAR_DESTINATION_LATITUDE = "39.0" -CAR_DESTINATION_LONGITUDE = "-77.1" +ORIGIN_LATITUDE = "38.9" +ORIGIN_LONGITUDE = "-77.04833" +DESTINATION_LATITUDE = "39.0" +DESTINATION_LONGITUDE = "-77.1" + +DEFAULT_CONFIG = { + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", +} diff --git a/tests/components/here_travel_time/fixtures/car_response.json b/tests/components/here_travel_time/fixtures/car_response.json index cd479b2c947..99a2d9ef051 100644 --- a/tests/components/here_travel_time/fixtures/car_response.json +++ b/tests/components/here_travel_time/fixtures/car_response.json @@ -1,304 +1,208 @@ { - "response": { - "metaInfo": { - "timestamp": "2019-07-19T07:38:39Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4446", - "interfaceVersion": "2.6.64", - "availableMapVersion": ["8.30.98.154"] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "+732182239", - "mappedPosition": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "originalPosition": { - "latitude": 38.9, - "longitude": -77.0483301 - }, - "type": "stopOver", - "spot": 0.4946237, - "sideOfStreet": "right", - "mappedRoadName": "22nd St NW", - "label": "22nd St NW", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+942865877", - "mappedPosition": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "originalPosition": { - "latitude": 38.9999999, - "longitude": -77.1000001 - }, - "type": "stopOver", - "spot": 1, - "sideOfStreet": "left", - "mappedRoadName": "Service Rd S", - "label": "Service Rd S", - "shapeIndex": 279, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": ["car"], - "trafficMode": "enabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "+732182239", - "mappedPosition": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "originalPosition": { - "latitude": 38.9, - "longitude": -77.0483301 - }, - "type": "stopOver", - "spot": 0.4946237, - "sideOfStreet": "right", - "mappedRoadName": "22nd St NW", - "label": "22nd St NW", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+942865877", - "mappedPosition": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "originalPosition": { - "latitude": 38.9999999, - "longitude": -77.1000001 - }, - "type": "stopOver", - "spot": 1, - "sideOfStreet": "left", - "mappedRoadName": "Service Rd S", - "label": "Service Rd S", - "shapeIndex": 279, - "source": "user" - }, - "length": 23903, - "travelTime": 1884, - "maneuver": [ - { - "position": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "instruction": "Head toward I St NW on 22nd St NW. Go for 279 m.", - "travelTime": 95, - "length": 279, - "id": "M1", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9021051, - "longitude": -77.048825 - }, - "instruction": "Turn left toward Pennsylvania Ave NW. Go for 71 m.", - "travelTime": 21, - "length": 71, - "id": "M2", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.902545, - "longitude": -77.0494151 - }, - "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 352 m.", - "travelTime": 90, - "length": 352, - "id": "M3", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9026523, - "longitude": -77.0529449 - }, - "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", - "travelTime": 30, - "length": 201, - "id": "M4", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9025235, - "longitude": -77.0552516 - }, - "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", - "travelTime": 131, - "length": 1381, - "id": "M5", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9050448, - "longitude": -77.0701969 - }, - "instruction": "Turn left onto M St NW. Go for 784 m.", - "travelTime": 78, - "length": 784, - "id": "M6", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9060318, - "longitude": -77.0790696 - }, - "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", - "travelTime": 277, - "length": 4230, - "id": "M7", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9303219, - "longitude": -77.1117926 - }, - "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", - "travelTime": 55, - "length": 844, - "id": "M8", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9368558, - "longitude": -77.1166742 - }, - "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", - "travelTime": 298, - "length": 4652, - "id": "M9", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9706838, - "longitude": -77.1461463 - }, - "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", - "travelTime": 91, - "length": 2069, - "id": "M10", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9858222, - "longitude": -77.1571326 - }, - "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 5.5 km.", - "travelTime": 238, - "length": 5538, - "id": "M11", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 39.0153587, - "longitude": -77.1221781 - }, - "instruction": "Take exit 36 toward Bethesda onto MD-187 S (Old Georgetown Rd). Go for 2.4 km.", - "travelTime": 211, - "length": 2365, - "id": "M12", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9981818, - "longitude": -77.1093571 - }, - "instruction": "Turn left onto Lincoln Dr. Go for 506 m.", - "travelTime": 127, - "length": 506, - "id": "M13", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9987397, - "longitude": -77.1037138 - }, - "instruction": "Turn right onto Service Rd W. Go for 121 m.", - "travelTime": 36, - "length": 121, - "id": "M14", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9976454, - "longitude": -77.1036172 - }, - "instruction": "Turn left onto Service Rd S. Go for 510 m.", - "travelTime": 106, - "length": 510, - "id": "M15", - "_type": "PrivateTransportManeuverType" - }, - { - "position": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "instruction": "Arrive at Service Rd S. Your destination is on the left.", - "travelTime": 0, - "length": 0, - "id": "M16", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 23903, - "trafficTime": 1861, - "baseTime": 1803, - "flags": [ - "noThroughRoad", - "motorway", - "builtUpArea", - "park", - "privateRoad" - ], - "text": "The trip takes 23.9 km and 31 mins.", - "travelTime": 1861, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us", - "sourceAttribution": { - "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", - "supplier": [ + "routes": [ + { + "id": "bace556c-2a16-4d93-a6c0-f595bbc89aa6", + "sections": [ { - "title": "HERE Technologies", - "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" + "id": "3ef521f3-7f88-480b-9b84-567a352c323c", + "type": "vehicle", + "departure": { + "time": "2022-10-24T05:53:00-04:00", + "place": { + "type": "place", + "location": { + "lat": 38.8999937, + "lng": -77.0479682 + }, + "originalLocation": { + "lat": 38.9, + "lng": -77.0483301 + } + } + }, + "arrival": { + "time": "2022-10-24T06:22:36-04:00", + "place": { + "type": "place", + "location": { + "lat": 38.99997, + "lng": -77.10014 + }, + "originalLocation": { + "lat": 38.9999999, + "lng": -77.1000001 + } + } + }, + "summary": { + "duration": 1776, + "length": 13682, + "baseDuration": 1571 + }, + "polyline": "BG0xomqC_p0-yE7ZXA_dA3XArnBTnpBkXAsYUkXAo4BTgZAA8GoB0F8BoG4DwHkDsEgFsEsEwC4D8BwCoB8BUwCU4DU0FT4DTsE7BkD7BwC7BgFzFwCrEkNwMkNwMwW8VgKsJoLAgPA4cUgeAwqBA0PAwHA8GA0yBAwbU4NA0FAgPAsTAoBAsnBAsEAkXAwvBU8VoBwMoBwMjD4NUkIUkD3IsEvMsEnLkI7V8GzUsE7LkD3IsEUoBAwCAoBAwCT8BToBTwC7BoBnB8BvCoB7BoBvCoBjDUvCA_EwCnLwC3IwCvHwC3I4D_E4DrE4D_EwCjD8BvCwCjDwCjDwC3DgFvHgFvH0KnQwRrY4XvgBkInLkDrEwCjDwHzK8GrJ0FjI0KrOgKjNwHzKsJjN8QjXkNjSwlBnzBoazjBwbnkB4D_EgenpBwCjDgFnGgF7G4cjmBoB7B4DnGwRzZkSrY4S_YgK3NoG3I0K_O4DzFoBnB4XvgB0K_OopBz3BoQrT4IjIoGrEkN_E8ajIkNrEoV_J0PrJsJvHwH7G4N3N0K3NgKjNkI7LsE7GwCrEoG_O4D_JsEvM0FnV0FjcoBvW8B3NwC_JwHvWUvCsJ7QokBnzBsTnasOrTwM7QsJjNoB7BoB7BsYvgB4I7LkNjSsTnawRrYgPnV4hB_sBgK3NoGrJwCjD0ZkDgZsEsEUkNwC4DU0UwCwlB0F0K8Bk_B0K4hBoG4I8B0FUgZkD0UwC8GUsYAkSAkSnB0KvC4rBjSkNzF0PvHwR3IoBToQjI0UnLwRrJwWvMwCnBozB3c4rB3XsT_JoVzKgjB_ToQrJsJzF8G3D0Z_OoL7GkrB3XoV7L0jBrTkIrE0P3IwCnBwWvM8VvMoBTwW7LkIrE4I_E4NvHoGjDsTnLwHrEkI_E4D7BoBT0FjDwMvHoBT4IrE4D7BsdzP0FjDgKzFoL7G4I_E8BnBoa_O4X3NoQ3IsTzK8BnBsJ_EwCnBgKrEwHjDsJvCwH7B8anG8QjD0PrE0ZnGoajI4D7B8zBvbgKrE8BnBwWnLwR3I0K_EoQ3I4I_EoLnGoBT4IrEgK_EofzP4X7L0UzKsxBzZkIrE4IrEoVzKsTrJ8QjIoGjDwH3DkcrOwWnLwR3I0UzKsYvMoajN08BnfwW7LwRjD0PnG0PnGwHjD0P7GgjBvRoVnLoVnL0PjI8VnLkmB3SoVzK8BnB4S3IoajN0ejN4SjIwHvCkS7GoBTwR_E8QrEoV_Ek6B7L8GnBoa3DwgB3D84B_J8zB7G03B_JsnBnGoQvCsYrEwHnB08B_Jo4BrJoVrE8L3D8GvC8QnG4NnGoQ7GkSjIsT_JkSrJ0UzK0Z3NgFvCwMnGoLzF0U_J4D7BwWnL4SrJ4D7BkmB3SwMnGoLzFkDnBkIrE0FvCof_O8ajN8V_JgK_EgZnL4N7G8QjI0Z3IkNrE8BToGjD0F3DwHzFsJvCwgBjIgU_EwMjD4DT4DTgFnBkhB3IoLjDoBAwqBrJ4DTwqB3IwHnBsTrEwH7BkS3DkI7BwCT4X_E8BToBA4I7BgKvCoQjDsY_E8azF0PjD8LjDwMvCgUzF0enGwR3DkI7B0UrEgUrEwR3D0PjDoQjD8L7BoVjD0P7B8GTsO7BgejD8Q7BoQ7BwMnBsJnBwHTgejDkSnBgPnBkNT0UnBoBAwHTwHT0UvC4N7B0KT4DT8BAT3DA_ET_d8BrOwC_J8GnQgPjXsEzFsJ7QsJ3XwHvWU_OoBvWAnGTzP_iBjSzK3D_JnB7LAvW8B7LUv5BsEoGgPoGsJ0FoGsEwC4D8BkDU8VToGA4SoGwHnBsJ3DoQA4DT8L7BgFkD8BwCUsE", + "spans": [ + { + "offset": 0, + "names": [ + { + "value": "22nd St NW", + "language": "en" + } + ] + }, + { + "offset": 1, + "names": [ + { + "value": "H St NW", + "language": "en" + } + ] + }, + { + "offset": 5, + "names": [ + { + "value": "23rd St NW", + "language": "en" + } + ] + }, + { + "offset": 10, + "names": [ + { + "value": "Washington Cir NW", + "language": "en" + } + ] + }, + { + "offset": 29, + "names": [ + { + "value": "New Hampshire Ave NW", + "language": "en" + } + ] + }, + { + "offset": 33, + "names": [ + { + "value": "22nd St NW", + "language": "en" + } + ] + }, + { + "offset": 57, + "names": [ + { + "value": "Massachusetts Ave NW", + "language": "en" + } + ] + }, + { + "offset": 64, + "names": [ + { + "value": "Massachusetts Ave NW", + "language": "en" + }, + { + "value": "Sheridan Cir NW", + "language": "en" + } + ] + }, + { + "offset": 78, + "names": [ + { + "value": "Sheridan Cir NW", + "language": "en" + }, + { + "value": "Massachusetts Ave NW", + "language": "en" + } + ] + }, + { + "offset": 79, + "names": [ + { + "value": "Massachusetts Ave NW", + "language": "en" + } + ] + }, + { + "offset": 174, + "names": [ + { + "value": "Wisconsin Ave NW", + "language": "en" + } + ] + }, + { + "offset": 291, + "names": [ + { + "value": "Wisconsin Ave", + "language": "en" + } + ] + }, + { + "offset": 408, + "names": [ + { + "value": "Rockville Pike", + "language": "en" + }, + { + "value": "Wisconsin Ave", + "language": "en" + } + ] + }, + { + "offset": 428, + "names": [ + { + "value": "South Dr", + "language": "en" + } + ] + }, + { + "offset": 443, + "names": [ + { + "value": "Center Dr", + "language": "en" + } + ] + }, + { + "offset": 450, + "names": [ + { + "value": "Service Rd S", + "language": "en" + } + ] + } + ], + "transport": { + "mode": "car" + } } ] } - } + ] } diff --git a/tests/components/here_travel_time/fixtures/empty_attribution_response.json b/tests/components/here_travel_time/fixtures/empty_attribution_response.json deleted file mode 100644 index cc1bb20a373..00000000000 --- a/tests/components/here_travel_time/fixtures/empty_attribution_response.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "response": { - "metaInfo": { - "timestamp": "2019-07-19T07:38:39Z", - "mapVersion": "8.30.98.154", - "moduleVersion": "7.2.201928-4446", - "interfaceVersion": "2.6.64", - "availableMapVersion": ["8.30.98.154"] - }, - "route": [ - { - "waypoint": [ - { - "linkId": "+732182239", - "mappedPosition": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "originalPosition": { - "latitude": 38.9, - "longitude": -77.0483301 - }, - "type": "stopOver", - "spot": 0.4946237, - "sideOfStreet": "right", - "mappedRoadName": "22nd St NW", - "label": "22nd St NW", - "shapeIndex": 0, - "source": "user" - }, - { - "linkId": "+942865877", - "mappedPosition": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "originalPosition": { - "latitude": 38.9999999, - "longitude": -77.1000001 - }, - "type": "stopOver", - "spot": 1, - "sideOfStreet": "left", - "mappedRoadName": "Service Rd S", - "label": "Service Rd S", - "shapeIndex": 279, - "source": "user" - } - ], - "mode": { - "type": "fastest", - "transportModes": ["car"], - "trafficMode": "enabled", - "feature": [] - }, - "leg": [ - { - "start": { - "linkId": "+732182239", - "mappedPosition": { - "latitude": 38.9, - "longitude": -77.0488358 - }, - "originalPosition": { - "latitude": 38.9, - "longitude": -77.0483301 - }, - "type": "stopOver", - "spot": 0.4946237, - "sideOfStreet": "right", - "mappedRoadName": "22nd St NW", - "label": "22nd St NW", - "shapeIndex": 0, - "source": "user" - }, - "end": { - "linkId": "+942865877", - "mappedPosition": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "originalPosition": { - "latitude": 38.9999999, - "longitude": -77.1000001 - }, - "type": "stopOver", - "spot": 1, - "sideOfStreet": "left", - "mappedRoadName": "Service Rd S", - "label": "Service Rd S", - "shapeIndex": 279, - "source": "user" - }, - "length": 23903, - "travelTime": 1884, - "maneuver": [ - { - "position": { - "latitude": 38.9999735, - "longitude": -77.100141 - }, - "instruction": "Arrive at Service Rd S. Your destination is on the left.", - "travelTime": 0, - "length": 0, - "id": "M16", - "_type": "PrivateTransportManeuverType" - } - ] - } - ], - "summary": { - "distance": 23903, - "trafficTime": 1861, - "baseTime": 1803, - "flags": [ - "noThroughRoad", - "motorway", - "builtUpArea", - "park", - "privateRoad" - ], - "text": "The trip takes 23.9 km and 31 mins.", - "travelTime": 1861, - "_type": "RouteSummaryType" - } - } - ], - "language": "en-us", - "sourceAttribution": {} - } -} diff --git a/tests/components/here_travel_time/fixtures/no_attribution_transit_route_response.json b/tests/components/here_travel_time/fixtures/no_attribution_transit_route_response.json new file mode 100644 index 00000000000..1057a4a66b1 --- /dev/null +++ b/tests/components/here_travel_time/fixtures/no_attribution_transit_route_response.json @@ -0,0 +1,145 @@ +{ + "routes": [ + { + "id": "C0", + "sections": [ + { + "id": "C0-S0", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 1111, + "instruction": "Head west on Wilhelm-Fay-Straße. Go for 1.1 km.", + "length": 1099, + "offset": 0 + }, + { + "action": "roundaboutExit", + "duration": 73, + "instruction": "Walk right around the roundabout and turn at the 1st street Frankfurter Straße. Go for 63 m.", + "length": 63, + "offset": 40, + "exit": 1, + "direction": "right" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Frankfurter Straße. Your destination is on the left.", + "length": 0, + "offset": 47 + } + ], + "travelSummary": { + "duration": 1140, + "length": 1162 + }, + "departure": { + "time": "2022-07-19T15:39:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.127787, + "lng": 8.582082 + } + } + }, + "arrival": { + "time": "2022-07-19T15:58:00+02:00", + "place": { + "name": "Eschborn Alfred-Herrhausen-Allee", + "type": "station", + "location": { + "lat": 50.135176, + "lng": 8.572745 + }, + "id": "110439568" + } + }, + "polyline": "BGwhxz_Cgv5rQwDrQ8BvHkNr2BsE7LkI7V0FrO4I7QgF3IoGzKkSjXsTzU0FzFkXnV0tB7pBkN3N0FzF4DjD0KrJwW3S0UrJwWnG4cjI0FjDkDvC8BnB0F_EsErEkDrEkIrJ8G_J4IrOsJ_O8VzjB8Q7asJnQwbztB8G7L0FrJsE7GkNvW8BwC4D8BsEnB8BjDgFkIkD0FqHwL", + "transport": { + "mode": "pedestrian" + } + }, + { + "id": "C0-S3", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 166, + "instruction": "Head southwest on Hunsrückstraße. Go for 155 m.", + "length": 155, + "offset": 0 + }, + { + "action": "turn", + "duration": 91, + "instruction": "Turn right onto Stolberger Straße. Go for 82 m.", + "length": 82, + "offset": 4, + "direction": "right", + "severity": "quite" + }, + { + "action": "turn", + "duration": 476, + "instruction": "Turn left onto Horchheimer Straße. Go for 466 m.", + "length": 466, + "offset": 9, + "direction": "left", + "severity": "quite" + }, + { + "action": "turn", + "duration": 18, + "instruction": "Turn left onto Hessenring. Go for 18 m.", + "length": 18, + "offset": 21, + "direction": "left", + "severity": "quite" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Hessenring. Your destination is on the left.", + "length": 0, + "offset": 22 + } + ], + "travelSummary": { + "duration": 720, + "length": 721 + }, + "departure": { + "time": "2022-07-19T17:15:00+02:00", + "place": { + "name": "Wiesbaden-Nordenstadt Stolberger Straße", + "type": "station", + "location": { + "lat": 50.060615, + "lng": 8.344163 + }, + "id": "110812533" + } + }, + "arrival": { + "time": "2022-07-19T17:27:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.060941, + "lng": 8.336477 + } + } + }, + "polyline": "BG20uv_C4lp9P9nBn-B3DzFvC3DnQjX8GvHkDvC4DnBoGoBkX8L0F3cwCnLkN7iCnBzhCAnG8B3wBsEzZkD_OwCzP8GnkBwHjmB8GvlBhJ1G", + "transport": { + "mode": "pedestrian" + } + } + ] + } + ] +} diff --git a/tests/components/here_travel_time/fixtures/transit_route_response.json b/tests/components/here_travel_time/fixtures/transit_route_response.json new file mode 100644 index 00000000000..72b04a2d10e --- /dev/null +++ b/tests/components/here_travel_time/fixtures/transit_route_response.json @@ -0,0 +1,153 @@ +{ + "routes": [ + { + "id": "C0", + "sections": [ + { + "id": "C0-S0", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 1111, + "instruction": "Head west on Wilhelm-Fay-Straße. Go for 1.1 km.", + "length": 1099, + "offset": 0 + }, + { + "action": "roundaboutExit", + "duration": 73, + "instruction": "Walk right around the roundabout and turn at the 1st street Frankfurter Straße. Go for 63 m.", + "length": 63, + "offset": 40, + "exit": 1, + "direction": "right" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Frankfurter Straße. Your destination is on the left.", + "length": 0, + "offset": 47 + } + ], + "travelSummary": { + "duration": 1140, + "length": 1162 + }, + "departure": { + "time": "2022-07-19T15:39:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.127787, + "lng": 8.582082 + } + } + }, + "arrival": { + "time": "2022-07-19T15:58:00+02:00", + "place": { + "name": "Eschborn Alfred-Herrhausen-Allee", + "type": "station", + "location": { + "lat": 50.135176, + "lng": 8.572745 + }, + "id": "110439568" + } + }, + "polyline": "BGwhxz_Cgv5rQwDrQ8BvHkNr2BsE7LkI7V0FrO4I7QgF3IoGzKkSjXsTzU0FzFkXnV0tB7pBkN3N0FzF4DjD0KrJwW3S0UrJwWnG4cjI0FjDkDvC8BnB0F_EsErEkDrEkIrJ8G_J4IrOsJ_O8VzjB8Q7asJnQwbztB8G7L0FrJsE7GkNvW8BwC4D8BsEnB8BjDgFkIkD0FqHwL", + "transport": { + "mode": "pedestrian" + } + }, + { + "id": "C0-S3", + "type": "pedestrian", + "actions": [ + { + "action": "depart", + "duration": 166, + "instruction": "Head southwest on Hunsrückstraße. Go for 155 m.", + "length": 155, + "offset": 0 + }, + { + "action": "turn", + "duration": 91, + "instruction": "Turn right onto Stolberger Straße. Go for 82 m.", + "length": 82, + "offset": 4, + "direction": "right", + "severity": "quite" + }, + { + "action": "turn", + "duration": 476, + "instruction": "Turn left onto Horchheimer Straße. Go for 466 m.", + "length": 466, + "offset": 9, + "direction": "left", + "severity": "quite" + }, + { + "action": "turn", + "duration": 18, + "instruction": "Turn left onto Hessenring. Go for 18 m.", + "length": 18, + "offset": 21, + "direction": "left", + "severity": "quite" + }, + { + "action": "arrive", + "duration": 0, + "instruction": "Arrive at Hessenring. Your destination is on the left.", + "length": 0, + "offset": 22 + } + ], + "travelSummary": { + "duration": 720, + "length": 721 + }, + "departure": { + "time": "2022-07-19T17:15:00+02:00", + "place": { + "name": "Wiesbaden-Nordenstadt Stolberger Straße", + "type": "station", + "location": { + "lat": 50.060615, + "lng": 8.344163 + }, + "id": "110812533" + } + }, + "arrival": { + "time": "2022-07-19T17:27:00+02:00", + "place": { + "type": "place", + "location": { + "lat": 50.060941, + "lng": 8.336477 + } + } + }, + "polyline": "BG20uv_C4lp9P9nBn-B3DzFvC3DnQjX8GvHkDvC4DnBoGoBkX8L0F3cwCnLkN7iCnBzhCAnG8B3wBsEzZkD_OwCzP8GnkBwHjmB8GvlBhJ1G", + "transport": { + "mode": "pedestrian" + }, + "attributions": [ + { + "id": "R00370b-C0-S2-link-0", + "href": "http://creativecommons.org/licenses/by/3.0/it/", + "text": "Some line names used in this product or service were edited to align with official transportation maps.", + "type": "disclaimer" + } + ] + } + ] + } + ] +} diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 120ffd828bc..42add4192e5 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -1,8 +1,7 @@ """Test the HERE Travel Time config flow.""" from unittest.mock import patch -from herepy import HEREError -from herepy.routing_api import InvalidCredentialsError +from here_routing import HERERoutingError, HERERoutingUnauthorizedError import pytest from homeassistant import config_entries, data_entry_flow @@ -15,34 +14,21 @@ from homeassistant.components.here_travel_time.const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, DOMAIN, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_CAR, - TRAVEL_MODE_PUBLIC_TIME_TABLE, -) -from homeassistant.const import ( - CONF_API_KEY, - CONF_MODE, - CONF_NAME, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, + TRAVEL_MODE_PUBLIC, ) +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.util.unit_system import ( - METRIC_SYSTEM, - US_CUSTOMARY_SYSTEM, - UnitSystem, -) from .const import ( API_KEY, - CAR_DESTINATION_LATITUDE, - CAR_DESTINATION_LONGITUDE, - CAR_ORIGIN_LATITUDE, - CAR_ORIGIN_LONGITUDE, + DEFAULT_CONFIG, + DESTINATION_LATITUDE, + DESTINATION_LONGITUDE, + ORIGIN_LATITUDE, + ORIGIN_LONGITUDE, ) from tests.common import MockConfigEntry @@ -83,12 +69,12 @@ async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.Flo domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, - CONF_MODE: TRAVEL_MODE_PUBLIC_TIME_TABLE, + CONF_MODE: TRAVEL_MODE_PUBLIC, CONF_NAME: "test", }, ) @@ -99,9 +85,7 @@ async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.Flo result = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={ - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, }, ) return result @@ -120,8 +104,8 @@ async def origin_step_result_fixture( origin_menu_result["flow_id"], { "origin": { - "latitude": float(CAR_ORIGIN_LATITUDE), - "longitude": float(CAR_ORIGIN_LONGITUDE), + "latitude": float(ORIGIN_LATITUDE), + "longitude": float(ORIGIN_LONGITUDE), "radius": 3.0, } }, @@ -170,8 +154,8 @@ async def test_step_origin_coordinates( menu_result["flow_id"], { "origin": { - "latitude": float(CAR_ORIGIN_LATITUDE), - "longitude": float(CAR_ORIGIN_LONGITUDE), + "latitude": float(ORIGIN_LATITUDE), + "longitude": float(ORIGIN_LONGITUDE), "radius": 3.0, } }, @@ -210,8 +194,8 @@ async def test_step_destination_coordinates( menu_result["flow_id"], { "destination": { - "latitude": float(CAR_DESTINATION_LATITUDE), - "longitude": float(CAR_DESTINATION_LONGITUDE), + "latitude": float(DESTINATION_LATITUDE), + "longitude": float(DESTINATION_LONGITUDE), "radius": 3.0, } }, @@ -223,30 +207,20 @@ async def test_step_destination_coordinates( assert entry.data == { CONF_NAME: "test", CONF_API_KEY: API_KEY, - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_MODE: TRAVEL_MODE_CAR, } @pytest.mark.usefixtures("valid_response") -@pytest.mark.parametrize( - "unit_system, expected_unit_option", - [ - (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC), - (US_CUSTOMARY_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL), - ], -) async def test_step_destination_entity( hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult, - unit_system: UnitSystem, - expected_unit_option: str, ) -> None: """Test the origin coordinates step.""" - hass.config.units = unit_system menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_entity"} ) @@ -261,15 +235,13 @@ async def test_step_destination_entity( assert entry.data == { CONF_NAME: "test", CONF_API_KEY: API_KEY, - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), CONF_DESTINATION_ENTITY_ID: "zone.home", CONF_MODE: TRAVEL_MODE_CAR, } assert entry.options == { - CONF_UNIT_SYSTEM: expected_unit_option, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, CONF_ARRIVAL_TIME: None, CONF_DEPARTURE_TIME: None, } @@ -282,8 +254,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) with patch( - "herepy.RoutingApi.public_transport_timetable", - side_effect=InvalidCredentialsError, + "here_routing.HERERoutingApi.route", + side_effect=HERERoutingUnauthorizedError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -305,8 +277,8 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) with patch( - "herepy.RoutingApi.public_transport_timetable", - side_effect=HEREError, + "here_routing.HERERoutingApi.route", + side_effect=HERERoutingError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -327,15 +299,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", - data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), - CONF_API_KEY: API_KEY, - CONF_MODE: TRAVEL_MODE_CAR, - CONF_NAME: "test", - }, + data=DEFAULT_CONFIG, ) entry.add_to_hass(hass) @@ -351,9 +315,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, }, ) @@ -379,9 +341,7 @@ async def test_options_flow_arrival_time_step( assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, CONF_ARRIVAL_TIME: "08:00:00", } @@ -405,9 +365,7 @@ async def test_options_flow_departure_time_step( assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, CONF_DEPARTURE_TIME: "08:00:00", } @@ -424,7 +382,5 @@ async def test_options_flow_no_time_step( assert menu_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, } diff --git a/tests/components/here_travel_time/test_init.py b/tests/components/here_travel_time/test_init.py index 05b7f6983db..18ef2e45410 100644 --- a/tests/components/here_travel_time/test_init.py +++ b/tests/components/here_travel_time/test_init.py @@ -2,25 +2,11 @@ import pytest -from homeassistant.components.here_travel_time.config_flow import default_options -from homeassistant.components.here_travel_time.const import ( - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, - DOMAIN, - TRAVEL_MODE_CAR, -) -from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME +from homeassistant.components.here_travel_time.config_flow import DEFAULT_OPTIONS +from homeassistant.components.here_travel_time.const import DOMAIN from homeassistant.core import HomeAssistant -from .const import ( - API_KEY, - CAR_DESTINATION_LATITUDE, - CAR_DESTINATION_LONGITUDE, - CAR_ORIGIN_LATITUDE, - CAR_ORIGIN_LONGITUDE, -) +from .const import DEFAULT_CONFIG from tests.common import MockConfigEntry @@ -31,16 +17,8 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", - data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), - CONF_API_KEY: API_KEY, - CONF_MODE: TRAVEL_MODE_CAR, - CONF_NAME: "test", - }, - options=default_options(hass), + data=DEFAULT_CONFIG, + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 5cc4802d253..144ac063040 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,11 +1,17 @@ """The test for the HERE Travel Time sensor platform.""" from unittest.mock import MagicMock, patch -from herepy.here_enum import RouteMode -from herepy.routing_api import NoRouteFoundError +from here_routing import ( + HERERoutingError, + Place, + Return, + RoutingMode, + Spans, + TransportMode, +) import pytest -from homeassistant.components.here_travel_time.config_flow import default_options +from homeassistant.components.here_travel_time.config_flow import DEFAULT_OPTIONS from homeassistant.components.here_travel_time.const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, @@ -20,17 +26,19 @@ from homeassistant.components.here_travel_time.const import ( ICON_BICYCLE, ICON_CAR, ICON_PEDESTRIAN, - ICON_PUBLIC, ICON_TRUCK, - NO_ROUTE_ERROR_MESSAGE, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, - TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_PUBLIC, TRAVEL_MODE_TRUCK, ) +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + SensorStateClass, +) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, @@ -40,83 +48,51 @@ from homeassistant.const import ( CONF_API_KEY, CONF_MODE, CONF_NAME, - CONF_UNIT_SYSTEM, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, - LENGTH_MILES, TIME_MINUTES, + UnitOfLength, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.setup import async_setup_component from .const import ( API_KEY, - CAR_DESTINATION_LATITUDE, - CAR_DESTINATION_LONGITUDE, - CAR_ORIGIN_LATITUDE, - CAR_ORIGIN_LONGITUDE, + DEFAULT_CONFIG, + DESTINATION_LATITUDE, + DESTINATION_LONGITUDE, + ORIGIN_LATITUDE, + ORIGIN_LONGITUDE, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data @pytest.mark.parametrize( - "mode,icon,unit_system,arrival_time,departure_time,expected_duration,expected_distance,expected_duration_in_traffic,expected_distance_unit", + "mode,icon,arrival_time,departure_time", [ ( TRAVEL_MODE_CAR, ICON_CAR, - "metric", None, None, - "30", - 23.903, - "31", - LENGTH_KILOMETERS, ), ( TRAVEL_MODE_BICYCLE, ICON_BICYCLE, - "metric", None, None, - "30", - 23.903, - "30", - LENGTH_KILOMETERS, ), ( TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, - "imperial", None, - None, - "30", - 14.85263, - "30", - LENGTH_MILES, - ), - ( - TRAVEL_MODE_PUBLIC_TIME_TABLE, - ICON_PUBLIC, - "imperial", "08:00:00", - None, - "30", - 14.85263, - "30", - LENGTH_MILES, ), ( TRAVEL_MODE_TRUCK, ICON_TRUCK, - "metric", None, "08:00:00", - "30", - 23.903, - "31", - LENGTH_KILOMETERS, ), ], ) @@ -125,23 +101,18 @@ async def test_sensor( hass: HomeAssistant, mode, icon, - unit_system, arrival_time, departure_time, - expected_duration, - expected_distance, - expected_duration_in_traffic, - expected_distance_unit, ): """Test that sensor works.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: mode, CONF_NAME: "test", @@ -150,7 +121,6 @@ async def test_sensor( CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_ARRIVAL_TIME: arrival_time, CONF_DEPARTURE_TIME: departure_time, - CONF_UNIT_SYSTEM: unit_system, }, ) entry.add_to_hass(hass) @@ -161,56 +131,29 @@ async def test_sensor( duration = hass.states.get("sensor.test_duration") assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES - assert ( - duration.attributes.get(ATTR_ATTRIBUTION) - == "With the support of HERE Technologies. All information is provided without warranty of any kind." - ) assert duration.attributes.get(ATTR_ICON) == icon - assert duration.state == expected_duration + assert duration.state == "26" - assert ( - hass.states.get("sensor.test_duration_in_traffic").state - == expected_duration_in_traffic - ) - assert float(hass.states.get("sensor.test_distance").state) == pytest.approx( - expected_distance - ) - assert ( - hass.states.get("sensor.test_distance").attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == expected_distance_unit - ) - assert ( - hass.states.get("sensor.test_duration_in_traffic").state - == expected_duration_in_traffic - ) + assert float(hass.states.get("sensor.test_distance").state) == pytest.approx(13.682) + assert hass.states.get("sensor.test_duration_in_traffic").state == "30" assert hass.states.get("sensor.test_origin").state == "22nd St NW" assert ( hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) - == CAR_ORIGIN_LATITUDE + == "38.8999937" ) assert ( hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) - == CAR_ORIGIN_LONGITUDE - ) - - assert hass.states.get("sensor.test_origin").state == "22nd St NW" - assert ( - hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) - == CAR_ORIGIN_LATITUDE - ) - assert ( - hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) - == CAR_ORIGIN_LONGITUDE + == "-77.0479682" ) assert hass.states.get("sensor.test_destination").state == "Service Rd S" assert ( hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) - == CAR_DESTINATION_LATITUDE + == "38.99997" ) assert ( hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) - == CAR_DESTINATION_LONGITUDE + == "-77.10014" ) @@ -227,13 +170,13 @@ async def test_circular_ref(hass: HomeAssistant, caplog): unique_id="0123456789", data={ CONF_ORIGIN_ENTITY_ID: "test.first", - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, - options=default_options(hass), + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -242,25 +185,60 @@ async def test_circular_ref(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert "No coordinatnes found for test.first" in caplog.text + assert "No coordinates found for test.first" in caplog.text -@pytest.mark.usefixtures("empty_attribution_response") -async def test_no_attribution(hass: HomeAssistant): - """Test that an empty attribution is handled.""" +@pytest.mark.usefixtures("valid_response") +async def test_public_transport(hass: HomeAssistant): + """Test that public transport mode is handled.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, - CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_MODE: TRAVEL_MODE_PUBLIC, CONF_NAME: "test", }, - options=default_options(hass), + options={ + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_ARRIVAL_TIME: "08:00:00", + CONF_DEPARTURE_TIME: None, + }, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.test_duration").attributes.get(ATTR_ATTRIBUTION) + == "http://creativecommons.org/licenses/by/3.0/it/,Some line names used in this product or service were edited to align with official transportation maps." + ) + assert hass.states.get("sensor.test_distance").state == "1.883" + + +@pytest.mark.usefixtures("no_attribution_response") +async def test_no_attribution_response(hass: HomeAssistant): + """Test that no_attribution is handled.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_PUBLIC, + CONF_NAME: "test", + }, + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -280,8 +258,8 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): "zone": [ { "name": "Origin", - "latitude": CAR_ORIGIN_LATITUDE, - "longitude": CAR_ORIGIN_LONGITUDE, + "latitude": ORIGIN_LATITUDE, + "longitude": ORIGIN_LONGITUDE, "radius": 250, "passive": False, }, @@ -292,8 +270,8 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): "device_tracker.test", "not_home", { - "latitude": float(CAR_DESTINATION_LATITUDE), - "longitude": float(CAR_DESTINATION_LONGITUDE), + "latitude": float(DESTINATION_LATITUDE), + "longitude": float(DESTINATION_LONGITUDE), }, ) entry = MockConfigEntry( @@ -306,7 +284,7 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, - options=default_options(hass), + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -315,19 +293,17 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert hass.states.get("sensor.test_distance").state == "23.903" + assert hass.states.get("sensor.test_distance").state == "13.682" valid_response.assert_called_with( - [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], - [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE], - True, - [ - RouteMode[ROUTE_MODE_FASTEST], - RouteMode[TRAVEL_MODE_TRUCK], - RouteMode[TRAFFIC_MODE_ENABLED], - ], - arrival=None, - departure="now", + transport_mode=TransportMode.TRUCK, + origin=Place(ORIGIN_LATITUDE, ORIGIN_LONGITUDE), + destination=Place(DESTINATION_LATITUDE, DESTINATION_LONGITUDE), + routing_mode=RoutingMode.FAST, + arrival_time=None, + departure_time=None, + return_values=[Return.POLYINE, Return.SUMMARY], + spans=[Spans.NAMES], ) @@ -338,14 +314,14 @@ async def test_destination_entity_not_found(hass: HomeAssistant, caplog): domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), CONF_DESTINATION_ENTITY_ID: "device_tracker.test", CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, - options=default_options(hass), + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -354,7 +330,7 @@ async def test_destination_entity_not_found(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert "device_tracker.test are not valid coordinates" in caplog.text + assert "Could not find entity device_tracker.test" in caplog.text @pytest.mark.usefixtures("valid_response") @@ -365,13 +341,13 @@ async def test_origin_entity_not_found(hass: HomeAssistant, caplog): unique_id="0123456789", data={ CONF_ORIGIN_ENTITY_ID: "device_tracker.test", - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, - options=default_options(hass), + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -380,7 +356,7 @@ async def test_origin_entity_not_found(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert "device_tracker.test are not valid coordinates" in caplog.text + assert "Could not find entity device_tracker.test" in caplog.text @pytest.mark.usefixtures("valid_response") @@ -394,14 +370,14 @@ async def test_invalid_destination_entity_state(hass: HomeAssistant, caplog): domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), CONF_DESTINATION_ENTITY_ID: "device_tracker.test", CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, - options=default_options(hass), + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -410,7 +386,9 @@ async def test_invalid_destination_entity_state(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert "test_state are not valid coordinates" in caplog.text + assert ( + "device_tracker.test does not have valid coordinates: test_state" in caplog.text + ) @pytest.mark.usefixtures("valid_response") @@ -425,13 +403,13 @@ async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog): unique_id="0123456789", data={ CONF_ORIGIN_ENTITY_ID: "device_tracker.test", - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, - options=default_options(hass), + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -440,28 +418,32 @@ async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert "test_state are not valid coordinates" in caplog.text + assert ( + "device_tracker.test does not have valid coordinates: test_state" in caplog.text + ) async def test_route_not_found(hass: HomeAssistant, caplog): """Test that route not found error is correctly handled.""" with patch( - "herepy.RoutingApi.public_transport_timetable", - side_effect=NoRouteFoundError, + "here_routing.HERERoutingApi.route", + side_effect=HERERoutingError( + "Route calculation failed: Couldn't find a route." + ), ): entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", data={ - CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), - CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), - CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), - CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), CONF_API_KEY: API_KEY, CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, - options=default_options(hass), + options=DEFAULT_OPTIONS, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -469,4 +451,135 @@ async def test_route_not_found(hass: HomeAssistant, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert NO_ROUTE_ERROR_MESSAGE in caplog.text + assert "Route calculation failed: Couldn't find a route." in caplog.text + + +@pytest.mark.usefixtures("valid_response") +async def test_restore_state(hass): + """Test sensor restore state.""" + # Home assistant is not running yet + hass.state = CoreState.not_running + last_reset = "2022-11-29T00:00:00.000000+00:00" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.test_duration", + "1234", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + ), + { + "native_value": 1234, + "native_unit_of_measurement": TIME_MINUTES, + "icon": "mdi:car", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.test_duration_in_traffic", + "5678", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + ), + { + "native_value": 5678, + "native_unit_of_measurement": TIME_MINUTES, + "icon": "mdi:car", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.test_distance", + "123", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + ), + { + "native_value": 123, + "native_unit_of_measurement": UnitOfLength.KILOMETERS, + "icon": "mdi:car", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.test_origin", + "Origin Address 1", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_LATITUDE: ORIGIN_LATITUDE, + ATTR_LONGITUDE: ORIGIN_LONGITUDE, + }, + ), + { + "native_value": "Origin Address 1", + "native_unit_of_measurement": None, + ATTR_LATITUDE: ORIGIN_LATITUDE, + ATTR_LONGITUDE: ORIGIN_LONGITUDE, + "icon": "mdi:store-marker", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.test_destination", + "Destination Address 1", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_LATITUDE: DESTINATION_LATITUDE, + ATTR_LONGITUDE: DESTINATION_LONGITUDE, + }, + ), + { + "native_value": "Destination Address 1", + "native_unit_of_measurement": None, + "icon": "mdi:store-marker", + "last_reset": last_reset, + }, + ), + ], + ) + + # create and add entry + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id=DOMAIN, data=DEFAULT_CONFIG, options=DEFAULT_OPTIONS + ) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # restore from cache + state = hass.states.get("sensor.test_duration") + assert state.state == "1234" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.test_duration_in_traffic") + assert state.state == "5678" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.test_distance") + assert state.state == "123" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.test_origin") + assert state.state == "Origin Address 1" + + state = hass.states.get("sensor.test_destination") + assert state.state == "Destination Address 1" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 981ff3bc08d..f0c4da26231 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -10,10 +10,6 @@ import pytest from homeassistant.components import history from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp -from homeassistant.components.recorder.websocket_api import ( - ws_handle_get_statistics_during_period, - ws_handle_list_statistic_ids, -) from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder @@ -31,7 +27,6 @@ from tests.components.recorder.common import ( def test_setup(): """Test setup method of history.""" # Verification occurs in the fixture - pass def test_get_significant_states(hass_history): @@ -844,76 +839,6 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state( assert response_json[1][0]["entity_id"] == "light.cow" -async def test_statistics_during_period(recorder_mock, hass, hass_ws_client, caplog): - """Test history/statistics_during_period forwards to recorder.""" - now = dt_util.utcnow() - await async_setup_component(hass, "history", {}) - client = await hass_ws_client() - - # Test the WS API works and issues a warning - await client.send_json( - { - "id": 1, - "type": "history/statistics_during_period", - "start_time": now.isoformat(), - "end_time": now.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "hour", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {} - - assert ( - "WS API 'history/statistics_during_period' is deprecated and will be removed in " - "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead" - ) in caplog.text - - # Test the WS API forwards to recorder - with patch( - "homeassistant.components.history.recorder_ws.ws_handle_get_statistics_during_period", - wraps=ws_handle_get_statistics_during_period, - ) as ws_mock: - await client.send_json( - { - "id": 2, - "type": "history/statistics_during_period", - "start_time": now.isoformat(), - "end_time": now.isoformat(), - "statistic_ids": ["sensor.test"], - "period": "hour", - } - ) - await client.receive_json() - ws_mock.assert_awaited_once() - - -async def test_list_statistic_ids(recorder_mock, hass, hass_ws_client, caplog): - """Test history/list_statistic_ids forwards to recorder.""" - await async_setup_component(hass, "history", {}) - client = await hass_ws_client() - - # Test the WS API works and issues a warning - await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) - response = await client.receive_json() - assert response["success"] - assert response["result"] == [] - - assert ( - "WS API 'history/list_statistic_ids' is deprecated and will be removed in " - "Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead" - ) in caplog.text - - with patch( - "homeassistant.components.history.recorder_ws.ws_handle_list_statistic_ids", - wraps=ws_handle_list_statistic_ids, - ) as ws_mock: - await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) - await client.receive_json() - ws_mock.assert_called_once() - - async def test_history_during_period(recorder_mock, hass, hass_ws_client): """Test history_during_period.""" now = dt_util.utcnow() diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 8e89be00433..0c980bcf07e 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -179,6 +179,7 @@ class TestComponentsCore(unittest.TestCase): config.YAML_CONFIG_FILE: yaml.dump( { ha.DOMAIN: { + "country": "SE", # To avoid creating issue country_not_configured "latitude": 10, "longitude": 20, "customize": {"test.Entity": {"hello": "world"}}, diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_1.json b/tests/components/homeassistant_alerts/fixtures/alerts_1.json index 381a31d7a5d..0f480f66b31 100644 --- a/tests/components/homeassistant_alerts/fixtures/alerts_1.json +++ b/tests/components/homeassistant_alerts/fixtures/alerts_1.json @@ -30,6 +30,26 @@ "filename": "dark_sky.markdown", "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" }, + { + "title": "Supervisor November beta issue impacting users on Home Assistant beta/dev channels", + "created": "2022-11-16T06:00:00.000Z", + "integrations": [ + { + "package": "hassio" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.41" + }, + "supervisor": { + "package": "supervisor", + "affected_from_version": "2022.11.0", + "resolved_in_version": "2022.11.1" + }, + "filename": "hassio.markdown", + "alert_url": "https://alerts.home-assistant.io/#hassio.markdown" + }, { "title": "Hikvision Security Vulnerability", "created": "2021-09-20T22:08:00.000Z", diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index 6b8cb7bf475..41fdff425b3 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -36,13 +36,15 @@ async def setup_repairs(hass): @pytest.mark.parametrize( - "ha_version, expected_alerts", + "ha_version, supervisor_info, expected_alerts", ( ( "2022.7.0", + {"version": "2022.11.0"}, [ ("aladdin_connect.markdown", "aladdin_connect"), ("dark_sky.markdown", "darksky"), + ("hassio.markdown", "hassio"), ("hikvision.markdown", "hikvision"), ("hikvision.markdown", "hikvisioncam"), ("hive_us.markdown", "hive"), @@ -56,6 +58,7 @@ async def setup_repairs(hass): ), ( "2022.8.0", + {"version": "2022.11.1"}, [ ("dark_sky.markdown", "darksky"), ("hikvision.markdown", "hikvision"), @@ -71,6 +74,7 @@ async def setup_repairs(hass): ), ( "2021.10.0", + None, [ ("aladdin_connect.markdown", "aladdin_connect"), ("dark_sky.markdown", "darksky"), @@ -91,6 +95,7 @@ async def test_alerts( hass_ws_client, aioclient_mock: AiohttpClientMocker, ha_version, + supervisor_info, expected_alerts, ) -> None: """Test creating issues based on alerts.""" @@ -119,9 +124,18 @@ async def test_alerts( for domain in activated_components: hass.config.components.add(domain) + if supervisor_info is not None: + hass.config.components.add("hassio") + with patch( "homeassistant.components.homeassistant_alerts.__version__", ha_version, + ), patch( + "homeassistant.components.homeassistant_alerts.is_hassio", + return_value=supervisor_info is not None, + ), patch( + "homeassistant.components.homeassistant_alerts.get_supervisor_info", + return_value=supervisor_info, ): assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/homeassistant_hardware/__init__.py b/tests/components/homeassistant_hardware/__init__.py new file mode 100644 index 00000000000..8f2ab13e5d1 --- /dev/null +++ b/tests/components/homeassistant_hardware/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant Hardware integration.""" diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py new file mode 100644 index 00000000000..fd0ce2e761b --- /dev/null +++ b/tests/components/homeassistant_hardware/conftest.py @@ -0,0 +1,136 @@ +"""Test fixtures for the Home Assistant Hardware integration.""" +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_zha_config_flow_setup() -> Generator[None, None, None]: + """Mock the radio connection and probing of the ZHA config flow.""" + + def mock_probe(config: dict[str, Any]) -> None: + # The radio probing will return the correct baudrate + return {**config, "baudrate": 115200} + + mock_connect_app = MagicMock() + mock_connect_app.__aenter__.return_value.backups.backups = [MagicMock()] + mock_connect_app.__aenter__.return_value.backups.create_backup.return_value = ( + MagicMock() + ) + + with patch( + "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe + ), patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + return_value=mock_connect_app, + ), patch( + "homeassistant.components.zha.async_setup_entry", + return_value=True, + ): + yield + + +@pytest.fixture(name="addon_running") +def mock_addon_running(addon_store_info, addon_info): + """Mock add-on already running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "started" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed(addon_store_info, addon_info): + """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_store_info") +def addon_store_info_fixture(): + """Mock Supervisor add-on store info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + +@pytest.fixture(name="addon_info") +def addon_info_fixture(): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_info", + ) as addon_info: + addon_info.return_value = { + "hostname": None, + "options": {}, + "state": None, + "update_available": False, + "version": None, + } + yield addon_info + + +@pytest.fixture(name="set_addon_options") +def set_addon_options_fixture(): + """Mock set add-on options.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_set_addon_options" + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon_side_effect") +def install_addon_side_effect_fixture(addon_store_info, addon_info): + """Return the install add-on side effect.""" + + async def install_addon(hass, slug): + """Mock install add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + + return install_addon + + +@pytest.fixture(name="install_addon") +def mock_install_addon(install_addon_side_effect): + """Mock install add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_install_addon", + side_effect=install_addon_side_effect, + ) as install_addon: + yield install_addon + + +@pytest.fixture(name="start_addon") +def start_addon_fixture(): + """Mock start add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_start_addon" + ) as start_addon: + yield start_addon diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py new file mode 100644 index 00000000000..577ac25eb82 --- /dev/null +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -0,0 +1,788 @@ +"""Test the Home Assistant Hardware silabs multiprotocol addon manager.""" +from __future__ import annotations + +from collections.abc import Generator +from typing import Any +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon +from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult, FlowResultType + +from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform + +TEST_DOMAIN = "test" + + +class TestConfigFlow(ConfigFlow, domain=TEST_DOMAIN): + """Handle a config flow for the silabs multiprotocol add-on.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> TestOptionsFlow: + """Return the options flow.""" + return TestOptionsFlow(config_entry) + + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Test HW", data={}) + + +class TestOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): + """Handle an option flow for the silabs multiprotocol add-on.""" + + async def _async_serial_port_settings( + self, + ) -> silabs_multiprotocol_addon.SerialPortSettings: + """Return the radio serial port settings.""" + return silabs_multiprotocol_addon.SerialPortSettings( + device="/dev/ttyTEST123", + baudrate="115200", + flow_control=True, + ) + + async def _async_zha_physical_discovery(self) -> dict[str, Any]: + """Return ZHA discovery data when multiprotocol FW is not used. + + Passed to ZHA do determine if the ZHA config entry is connected to the radio + being migrated. + """ + return { + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } + } + + def _zha_name(self) -> str: + """Return the ZHA name.""" + return "Test Multi-PAN" + + +@pytest.fixture(autouse=True) +def config_flow_handler( + hass: HomeAssistant, current_request_with_host: Any +) -> Generator[TestConfigFlow, None, None]: + """Fixture for a test config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestConfigFlow}): + yield TestConfigFlow + + +async def test_option_flow_install_multi_pan_addon( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "/dev/ttyTEST123", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_install_multi_pan_addon_zha( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon when a zha config entry exists.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=ZHA_DOMAIN, + options={}, + title="Test", + ) + zha_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "/dev/ttyTEST123", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + # Check the ZHA config entry data is updated + assert zha_config_entry.data == { + "device": { + "path": "socket://core-silabs-multiprotocol:9999", + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default + }, + "radio_type": "ezsp", + } + assert zha_config_entry.title == "Test Multi-PAN" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_install_multi_pan_addon_zha_other_radio( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon when a zha config entry exists.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={ + "device": { + "path": "/dev/other_radio", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + }, + domain=ZHA_DOMAIN, + options={}, + title="Test HW", + ) + zha_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "/dev/ttyTEST123", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY + + # Check the ZHA entry data is not changed + assert zha_config_entry.data == { + "device": { + "path": "/dev/other_radio", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + } + + +async def test_option_flow_non_hassio( + hass: HomeAssistant, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_hassio" + + +async def test_option_flow_addon_installed_other_device( + hass: HomeAssistant, + addon_store_info, + addon_installed, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_installed_other_device" + + result = await hass.config_entries.options.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_addon_installed_same_device( + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "show_revert_guide" + + result = await hass.config_entries.options.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_do_not_install_multi_pan_addon( + hass: HomeAssistant, + addon_info, + addon_store_info, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": False, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_install_multi_pan_addon_install_fails( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + install_addon.side_effect = HassioAPIError("Boom") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "install_failed" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +async def test_option_flow_install_multi_pan_addon_start_fails( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + start_addon.side_effect = HassioAPIError("Boom") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "/dev/ttyTEST123", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "start_failed" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + +async def test_option_flow_install_multi_pan_addon_set_options_fails( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + set_addon_options.side_effect = HassioAPIError("Boom") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_set_config_failed" + + +async def test_option_flow_addon_info_fails( + hass: HomeAssistant, + addon_store_info, + addon_info, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + addon_store_info.side_effect = HassioAPIError("Boom") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_info_failed" + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaMultiPANMigrationHelper.async_initiate_migration", + side_effect=Exception("Boom!"), +) +async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( + mock_initiate_migration, + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=ZHA_DOMAIN, + options={}, + title="Test", + ) + zha_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "zha_migration_failed" + set_addon_options.assert_not_called() + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaMultiPANMigrationHelper.async_finish_migration", + side_effect=Exception("Boom!"), +) +async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( + mock_finish_migration, + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=ZHA_DOMAIN, + options={}, + title="Test", + ) + zha_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "/dev/ttyTEST123", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "zha_migration_failed" diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index cc606c9b988..2d333c62b2d 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,14 +1,138 @@ """Test fixtures for the Home Assistant Sky Connect integration.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import MagicMock, patch import pytest +@pytest.fixture(name="mock_usb_serial_by_id", autouse=True) +def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]: + """Mock usb serial by id.""" + with patch( + "homeassistant.components.zwave_js.config_flow.usb.get_serial_by_id" + ) as mock_usb_serial_by_id: + mock_usb_serial_by_id.side_effect = lambda x: x + yield mock_usb_serial_by_id + + @pytest.fixture(autouse=True) def mock_zha(): """Mock the zha integration.""" + mock_connect_app = MagicMock() + mock_connect_app.__aenter__.return_value.backups.backups = [MagicMock()] + mock_connect_app.__aenter__.return_value.backups.create_backup.return_value = ( + MagicMock() + ) + with patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + return_value=mock_connect_app, + ), patch( "homeassistant.components.zha.async_setup_entry", return_value=True, ): yield + + +@pytest.fixture(name="addon_running") +def mock_addon_running(addon_store_info, addon_info): + """Mock add-on already running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "started" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed(addon_store_info, addon_info): + """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_store_info") +def addon_store_info_fixture(): + """Mock Supervisor add-on store info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + +@pytest.fixture(name="addon_info") +def addon_info_fixture(): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_info", + ) as addon_info: + addon_info.return_value = { + "hostname": None, + "options": {}, + "state": None, + "update_available": False, + "version": None, + } + yield addon_info + + +@pytest.fixture(name="set_addon_options") +def set_addon_options_fixture(): + """Mock set add-on options.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_set_addon_options" + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon_side_effect") +def install_addon_side_effect_fixture(addon_store_info, addon_info): + """Return the install add-on side effect.""" + + async def install_addon(hass, slug): + """Mock install add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + + return install_addon + + +@pytest.fixture(name="install_addon") +def mock_install_addon(install_addon_side_effect): + """Mock install add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_install_addon", + side_effect=install_addon_side_effect, + ) as install_addon: + yield install_addon + + +@pytest.fixture(name="start_addon") +def start_addon_fixture(): + """Mock start add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_start_addon" + ) as start_addon: + yield start_addon diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index bbde732d201..c38edf00fa7 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,13 +1,18 @@ """Test the Home Assistant Sky Connect config flow.""" import copy -from unittest.mock import patch +from unittest.mock import Mock, patch from homeassistant.components import homeassistant_sky_connect, usb from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.components.zha.core.const import ( + CONF_DEVICE_PATH, + DOMAIN as ZHA_DOMAIN, + RadioType, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, MockModule, mock_integration USB_DATA = usb.UsbServiceInfo( device="bla_device", @@ -143,3 +148,190 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_unload_entry.mock_calls) == 1 + + +async def test_option_flow_install_multi_pan_addon( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={ + "device": USB_DATA.device, + "vid": USB_DATA.vid, + "pid": USB_DATA.pid, + "serial_number": USB_DATA.serial_number, + "manufacturer": USB_DATA.manufacturer, + "description": USB_DATA.description, + }, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "bla_device", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): + """Mock `detect_radio_type` that just sets the appropriate attributes.""" + + async def detect(self): + self.radio_type = radio_type + self.device_settings = radio_type.controller.SCHEMA_DEVICE( + {CONF_DEVICE_PATH: self.device_path} + ) + + return ret + + return detect + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + mock_detect_radio_type(), +) +async def test_option_flow_install_multi_pan_addon_zha( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon when a zha config entry exists.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={ + "device": USB_DATA.device, + "vid": USB_DATA.vid, + "pid": USB_DATA.pid, + "serial_number": USB_DATA.serial_number, + "manufacturer": USB_DATA.manufacturer, + "description": USB_DATA.description, + }, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={"device": {"path": "bla_device"}, "radio_type": "ezsp"}, + domain=ZHA_DOMAIN, + options={}, + title="Yellow", + ) + zha_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "bla_device", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + # Check the ZHA config entry data is updated + assert zha_config_entry.data == { + "device": { + "path": "socket://core-silabs-multiprotocol:9999", + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default + }, + "radio_type": "ezsp", + } + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 6226651133a..01f0e6ac5d7 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, MockModule, mock_integration CONFIG_ENTRY_DATA = { "device": "bla_device", @@ -25,8 +25,12 @@ CONFIG_ENTRY_DATA_2 = { } -async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: +async def test_hardware_info( + hass: HomeAssistant, hass_ws_client, addon_store_info +) -> None: """Test we can get the board info.""" + mock_integration(hass, MockModule("usb")) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, @@ -62,6 +66,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "hardware": [ { "board": None, + "config_entries": [config_entry.entry_id], "dongle": { "vid": "bla_vid", "pid": "bla_pid", @@ -74,6 +79,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: }, { "board": None, + "config_entries": [config_entry_2.entry_id], "dongle": { "vid": "bla_vid_2", "pid": "bla_pid_2", diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 05b883a9726..ebf1c74d9e0 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,11 +1,12 @@ """Test the Home Assistant Sky Connect integration.""" from collections.abc import Generator from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.components import zha +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -13,12 +14,12 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry CONFIG_ENTRY_DATA = { - "device": "bla_device", - "vid": "bla_vid", - "pid": "bla_pid", - "serial_number": "bla_serial_number", - "manufacturer": "bla_manufacturer", - "description": "bla_description", + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", } @@ -36,7 +37,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: with patch( "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe ), patch( - "homeassistant.components.zha.config_flow.BaseZhaFlow._connect_zigpy_app", + "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", return_value=mock_connect_app, ): yield @@ -46,7 +47,12 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: "onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1)) ) async def test_setup_entry( - mock_zha_config_flow_setup, hass: HomeAssistant, onboarded, num_entries, num_flows + mock_zha_config_flow_setup, + hass: HomeAssistant, + addon_store_info, + onboarded, + num_entries, + num_flows, ) -> None: """Test setup of a config entry, including setup of zha.""" # Setup the config entry @@ -67,6 +73,13 @@ async def test_setup_entry( await hass.async_block_till_done() assert len(mock_is_plugged_in.mock_calls) == 1 + matcher = mock_is_plugged_in.mock_calls[0].args[1] + assert matcher["vid"].isupper() + assert matcher["pid"].isupper() + assert matcher["serial_number"].islower() + assert matcher["manufacturer"].islower() + assert matcher["description"].islower() + # Finish setting up ZHA if num_entries > 0: zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") @@ -83,7 +96,9 @@ async def test_setup_entry( assert len(hass.config_entries.async_entries("zha")) == num_entries -async def test_setup_zha(mock_zha_config_flow_setup, hass: HomeAssistant) -> None: +async def test_setup_zha( + mock_zha_config_flow_setup, hass: HomeAssistant, addon_store_info +) -> None: """Test zha gets the right config.""" # Setup the config entry config_entry = MockConfigEntry( @@ -119,12 +134,114 @@ async def test_setup_zha(mock_zha_config_flow_setup, hass: HomeAssistant) -> Non "device": { "baudrate": 115200, "flow_control": "software", - "path": "bla_device", + "path": CONFIG_ENTRY_DATA["device"], }, "radio_type": "ezsp", } assert config_entry.options == {} - assert config_entry.title == "bla_description" + assert config_entry.title == CONFIG_ENTRY_DATA["description"] + + +async def test_setup_zha_multipan( + hass: HomeAssistant, addon_info, addon_running +) -> None: + """Test zha gets the right config.""" + addon_info.return_value["options"]["device"] = CONFIG_ENTRY_DATA["device"] + + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + + # Finish setting up ZHA + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + config_entry = hass.config_entries.async_entries("zha")[0] + assert config_entry.data == { + "device": { + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default + "path": "socket://core-silabs-multiprotocol:9999", + }, + "radio_type": "ezsp", + } + assert config_entry.options == {} + assert config_entry.title == "Sky Connect Multi-PAN" + + +async def test_setup_zha_multipan_other_device( + mock_zha_config_flow_setup, hass: HomeAssistant, addon_info, addon_running +) -> None: + """Test zha gets the right config.""" + addon_info.return_value["options"]["device"] = "/dev/not_our_sky_connect" + + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + + # Finish setting up ZHA + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + config_entry = hass.config_entries.async_entries("zha")[0] + assert config_entry.data == { + "device": { + "baudrate": 115200, + "flow_control": "software", + "path": CONFIG_ENTRY_DATA["device"], + }, + "radio_type": "ezsp", + } + assert config_entry.options == {} + assert config_entry.title == CONFIG_ENTRY_DATA["description"] async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: @@ -145,3 +262,58 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_is_plugged_in.mock_calls) == 1 assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_setup_entry_addon_info_fails( + hass: HomeAssistant, addon_store_info +) -> None: + """Test setup of a config entry when fetching addon info fails.""" + addon_store_info.side_effect = HassioAPIError("Boom") + + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_setup_entry_addon_not_running( + hass: HomeAssistant, addon_installed, start_addon +) -> None: + """Test the addon is started if it is not running.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "homeassistant.components.homeassistant_sky_connect.is_hassio", + side_effect=Mock(return_value=True), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.SETUP_RETRY + start_addon.assert_called_once() diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 52759ba6d89..62595c11fe1 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -15,15 +15,122 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: return {**config, "baudrate": 115200} mock_connect_app = MagicMock() - mock_connect_app.__aenter__.return_value.backups.backups = [] + mock_connect_app.__aenter__.return_value.backups.backups = [MagicMock()] + mock_connect_app.__aenter__.return_value.backups.create_backup.return_value = ( + MagicMock() + ) with patch( "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe ), patch( - "homeassistant.components.zha.config_flow.BaseZhaFlow._connect_zigpy_app", + "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", return_value=mock_connect_app, ), patch( "homeassistant.components.zha.async_setup_entry", return_value=True, ): yield + + +@pytest.fixture(name="addon_running") +def mock_addon_running(addon_store_info, addon_info): + """Mock add-on already running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "started" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed(addon_store_info, addon_info): + """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_store_info") +def addon_store_info_fixture(): + """Mock Supervisor add-on store info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + +@pytest.fixture(name="addon_info") +def addon_info_fixture(): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_info", + ) as addon_info: + addon_info.return_value = { + "hostname": None, + "options": {}, + "state": None, + "update_available": False, + "version": None, + } + yield addon_info + + +@pytest.fixture(name="set_addon_options") +def set_addon_options_fixture(): + """Mock set add-on options.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_set_addon_options" + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon_side_effect") +def install_addon_side_effect_fixture(addon_store_info, addon_info): + """Return the install add-on side effect.""" + + async def install_addon(hass, slug): + """Mock install add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + + return install_addon + + +@pytest.fixture(name="install_addon") +def mock_install_addon(install_addon_side_effect): + """Mock install add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_install_addon", + side_effect=install_addon_side_effect, + ) as install_addon: + yield install_addon + + +@pytest.fixture(name="start_addon") +def start_addon_fixture(): + """Mock start add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_start_addon" + ) as start_addon: + yield start_addon diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index e6d5da11806..53d1c5e974d 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -1,7 +1,8 @@ """Test the Home Assistant Yellow config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from homeassistant.components.homeassistant_yellow.const import DOMAIN +from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -56,3 +57,156 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() + + +async def test_option_flow_install_multi_pan_addon( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "/dev/ttyAMA1", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_install_multi_pan_addon_zha( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + set_addon_options, + start_addon, +) -> None: + """Test installing the multi pan addon when a zha config entry exists.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyAMA1"}, "radio_type": "ezsp"}, + domain=ZHA_DOMAIN, + options={}, + title="Yellow", + ) + zha_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "enable_multi_pan": True, + }, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_addon" + install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + set_addon_options.assert_called_once_with( + hass, + "core_silabs_multiprotocol", + { + "options": { + "autoflash_firmware": True, + "device": "/dev/ttyAMA1", + "baudrate": "115200", + "flow_control": True, + } + }, + ) + # Check the ZHA config entry data is updated + assert zha_config_entry.data == { + "device": { + "path": "socket://core-silabs-multiprotocol:9999", + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default + }, + "radio_type": "ezsp", + } + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "finish_addon_setup" + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 45d6fcabdfe..add83fde7c1 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -9,7 +9,9 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, MockModule, mock_integration -async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: +async def test_hardware_info( + hass: HomeAssistant, hass_ws_client, addon_store_info +) -> None: """Test we can get the board info.""" mock_integration(hass, MockModule("hassio")) @@ -48,6 +50,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "yellow", "revision": None, }, + "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Yellow", "url": None, @@ -57,7 +60,9 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: @pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) -async def test_hardware_info_fail(hass: HomeAssistant, hass_ws_client, os_info) -> None: +async def test_hardware_info_fail( + hass: HomeAssistant, hass_ws_client, os_info, addon_store_info +) -> None: """Test async_info raises if os_info is not as expected.""" mock_integration(hass, MockModule("hassio")) diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index bc36ae3cec2..4118c6dc654 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from homeassistant.components import zha +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -15,7 +16,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration "onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1)) ) async def test_setup_entry( - hass: HomeAssistant, onboarded, num_entries, num_flows + hass: HomeAssistant, onboarded, num_entries, num_flows, addon_store_info ) -> None: """Test setup of a config entry, including setup of zha.""" mock_integration(hass, MockModule("hassio")) @@ -53,8 +54,11 @@ async def test_setup_entry( assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows assert len(hass.config_entries.async_entries("zha")) == num_entries + # Test unloading the config entry + assert await hass.config_entries.async_unload(config_entry.entry_id) -async def test_setup_zha(hass: HomeAssistant) -> None: + +async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None: """Test zha gets the right config.""" mock_integration(hass, MockModule("hassio")) @@ -100,6 +104,106 @@ async def test_setup_zha(hass: HomeAssistant) -> None: assert config_entry.title == "Yellow" +async def test_setup_zha_multipan( + hass: HomeAssistant, addon_info, addon_running +) -> None: + """Test zha gets the right config.""" + mock_integration(hass, MockModule("hassio")) + + addon_info.return_value["options"]["device"] = "/dev/ttyAMA1" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + # Finish setting up ZHA + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + config_entry = hass.config_entries.async_entries("zha")[0] + assert config_entry.data == { + "device": { + "baudrate": 57600, # ZHA default + "flow_control": "software", # ZHA default + "path": "socket://core-silabs-multiprotocol:9999", + }, + "radio_type": "ezsp", + } + assert config_entry.options == {} + assert config_entry.title == "Yellow Multi-PAN" + + +async def test_setup_zha_multipan_other_device( + hass: HomeAssistant, addon_info, addon_running +) -> None: + """Test zha gets the right config.""" + mock_integration(hass, MockModule("hassio")) + + addon_info.return_value["options"]["device"] = "/dev/not_yellow_radio" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + # Finish setting up ZHA + zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") + assert len(zha_flows) == 1 + assert zha_flows[0]["step_id"] == "choose_formation_strategy" + + await hass.config_entries.flow.async_configure( + zha_flows[0]["flow_id"], + user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, + ) + await hass.async_block_till_done() + + config_entry = hass.config_entries.async_entries("zha")[0] + assert config_entry.data == { + "device": { + "baudrate": 115200, + "flow_control": "hardware", + "path": "/dev/ttyAMA1", + }, + "radio_type": "ezsp", + } + assert config_entry.options == {} + assert config_entry.title == "Yellow" + + async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: """Test setup of a config entry with wrong board type.""" mock_integration(hass, MockModule("hassio")) @@ -141,3 +245,55 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_setup_entry_addon_info_fails( + hass: HomeAssistant, addon_store_info +) -> None: + """Test setup of a config entry when fetching addon info fails.""" + mock_integration(hass, MockModule("hassio")) + addon_store_info.side_effect = HassioAPIError("Boom") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_setup_entry_addon_not_running( + hass: HomeAssistant, addon_installed, start_addon +) -> None: + """Test the addon is started if it is not running.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.SETUP_RETRY + start_addon.assert_called_once() diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index b0422a40f72..2800dfb9be7 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -21,7 +21,7 @@ def iid_storage(hass): @pytest.fixture() -def run_driver(hass, loop, iid_storage): +def run_driver(hass, event_loop, iid_storage): """Return a custom AccessoryDriver instance for HomeKit accessory init. This mock does not mock async_stop, so the driver will not be stopped @@ -41,12 +41,12 @@ def run_driver(hass, loop, iid_storage): bridge_name=BRIDGE_NAME, iid_storage=iid_storage, address="127.0.0.1", - loop=loop, + loop=event_loop, ) @pytest.fixture -def hk_driver(hass, loop, iid_storage): +def hk_driver(hass, event_loop, iid_storage): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" @@ -65,12 +65,12 @@ def hk_driver(hass, loop, iid_storage): bridge_name=BRIDGE_NAME, iid_storage=iid_storage, address="127.0.0.1", - loop=loop, + loop=event_loop, ) @pytest.fixture -def mock_hap(hass, loop, iid_storage, mock_zeroconf): +def mock_hap(hass, event_loop, iid_storage, mock_zeroconf): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" @@ -93,7 +93,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf): bridge_name=BRIDGE_NAME, iid_storage=iid_storage, address="127.0.0.1", - loop=loop, + loop=event_loop, ) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 32f4abe98f1..12113ada5cb 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -226,6 +226,18 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): "40", {ATTR_DEVICE_CLASS: "pm25"}, ), + ( + "NitrogenDioxideSensor", + "sensor.air_quality_nitrogen_dioxide", + "50", + {ATTR_DEVICE_CLASS: SensorDeviceClass.NITROGEN_DIOXIDE}, + ), + ( + "VolatileOrganicCompoundsSensor", + "sensor.air_quality_volatile_organic_compounds", + "55", + {ATTR_DEVICE_CLASS: SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS}, + ), ( "CarbonMonoxideSensor", "sensor.co", diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 4997a35910d..28dfe04932f 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -15,9 +15,11 @@ from homeassistant.components.homekit.type_sensors import ( CarbonMonoxideSensor, HumiditySensor, LightSensor, + NitrogenDioxideSensor, PM10Sensor, PM25Sensor, TemperatureSensor, + VolatileOrganicCompoundsSensor, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -155,24 +157,24 @@ async def test_pm10(hass, hk_driver): assert acc.char_density.value == 0 assert acc.char_quality.value == 0 - hass.states.async_set(entity_id, "34") + hass.states.async_set(entity_id, "54") await hass.async_block_till_done() - assert acc.char_density.value == 34 + assert acc.char_density.value == 54 assert acc.char_quality.value == 1 - hass.states.async_set(entity_id, "70") + hass.states.async_set(entity_id, "154") await hass.async_block_till_done() - assert acc.char_density.value == 70 + assert acc.char_density.value == 154 assert acc.char_quality.value == 2 - hass.states.async_set(entity_id, "110") + hass.states.async_set(entity_id, "254") await hass.async_block_till_done() - assert acc.char_density.value == 110 + assert acc.char_density.value == 254 assert acc.char_quality.value == 3 - hass.states.async_set(entity_id, "200") + hass.states.async_set(entity_id, "354") await hass.async_block_till_done() - assert acc.char_density.value == 200 + assert acc.char_density.value == 354 assert acc.char_quality.value == 4 hass.states.async_set(entity_id, "400") @@ -228,6 +230,104 @@ async def test_pm25(hass, hk_driver): assert acc.char_quality.value == 5 +async def test_no2(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_nitrogen_dioxide" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = NitrogenDioxideSensor( + hass, hk_driver, "Nitrogen Dioxide Sensor", entity_id, 2, None + ) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "30") + await hass.async_block_till_done() + assert acc.char_density.value == 30 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "60") + await hass.async_block_till_done() + assert acc.char_density.value == 60 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "80") + await hass.async_block_till_done() + assert acc.char_density.value == 80 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "90") + await hass.async_block_till_done() + assert acc.char_density.value == 90 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "100") + await hass.async_block_till_done() + assert acc.char_density.value == 100 + assert acc.char_quality.value == 5 + + +async def test_voc(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_volatile_organic_compounds" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = VolatileOrganicCompoundsSensor( + hass, hk_driver, "Volatile Organic Compounds Sensor", entity_id, 2, None + ) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "24") + await hass.async_block_till_done() + assert acc.char_density.value == 24 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "48") + await hass.async_block_till_done() + assert acc.char_density.value == 48 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "64") + await hass.async_block_till_done() + assert acc.char_density.value == 64 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "96") + await hass.async_block_till_done() + assert acc.char_density.value == 96 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "128") + await hass.async_block_till_done() + assert acc.char_density.value == 128 + assert acc.char_quality.value == 5 + + async def test_co(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = "sensor.co" diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index b30ba6236a9..ec30d541a93 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -21,6 +21,7 @@ from aiohomekit.zeroconf import HomeKitService from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.homekit_controller.const import ( CONTROLLER, + DEBOUNCE_COOLDOWN, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, IDENTIFIER_ACCESSORY_ID, @@ -146,6 +147,7 @@ class Helper: # If they are enabled, then HA will pick up the changes next time # we yield control await time_changed(self.hass, 60) + await time_changed(self.hass, DEBOUNCE_COOLDOWN) await self.hass.async_block_till_done() @@ -165,6 +167,7 @@ class Helper: async def poll_and_get_state(self) -> State: """Trigger a time based poll and return the current entity state.""" await time_changed(self.hass, 60) + await time_changed(self.hass, DEBOUNCE_COOLDOWN) state = self.hass.states.get(self.entity_id) assert state is not None diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 5e2c8249560..86af49a96c4 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -8,6 +8,7 @@ from aiohomekit.exceptions import AuthenticationError from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from bleak.exc import BleakError import pytest from homeassistant import config_entries @@ -743,6 +744,57 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) } +async def test_pair_unknown_errors(hass, controller): + """Test describing unknown errors.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + + assert get_flow_context(hass, result) == { + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, + "unique_id": "00:00:00:00:00:00", + "source": config_entries.SOURCE_ZEROCONF, + } + + # User initiates pairing - this triggers the device to show a pairing code + # and then HA to show a pairing form + finish_pairing = unittest.mock.AsyncMock( + side_effect=BleakError("The bluetooth connection failed") + ) + with patch.object(device, "async_start_pairing", return_value=finish_pairing): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert get_flow_context(hass, result) == { + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, + "unique_id": "00:00:00:00:00:00", + "source": config_entries.SOURCE_ZEROCONF, + } + + # User enters pairing code + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "111-22-333"} + ) + assert result["type"] == "form" + assert result["errors"]["pairing_code"] == "pairing_failed" + assert ( + result["description_placeholders"]["error"] == "The bluetooth connection failed" + ) + + assert get_flow_context(hass, result) == { + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, + "unique_id": "00:00:00:00:00:00", + "source": config_entries.SOURCE_ZEROCONF, + "pairing": True, + } + + async def test_user_works(hass, controller): """Test user initiated disovers devices.""" setup_mock_accessory(controller) diff --git a/tests/components/homematicip_cloud/test_button.py b/tests/components/homematicip_cloud/test_button.py new file mode 100644 index 00000000000..c399bc99327 --- /dev/null +++ b/tests/components/homematicip_cloud/test_button.py @@ -0,0 +1,39 @@ +"""Tests for HomematicIP Cloud button.""" + +from unittest.mock import patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.util import dt as dt_util + +from .helper import get_and_check_entity_basics + + +async def test_hmip_garage_door_controller_button(hass, default_mock_hap_factory): + """Test HomematicipGarageDoorControllerButton.""" + entity_id = "button.garagentor" + entity_name = "Garagentor" + device_model = "HmIP-WGC" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) + + get_and_check_entity_basics(hass, mock_hap, entity_id, entity_name, device_model) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.utcnow", return_value=now): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == now.isoformat() diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 44b91c4ed47..327f53f129c 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -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) == 262 + assert len(mock_hap.hmip_device_by_entity_id) == 267 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 823508d5fee..33da0f217ae 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -193,6 +193,50 @@ async def test_hmip_temperature_sensor3(hass, default_mock_hap_factory): assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 +async def test_hmip_thermostat_evo_heating(hass, default_mock_hap_factory): + """Test HomematicipHeatingThermostat for HmIP-eTRV-E.""" + entity_id = "sensor.thermostat_evo_heating" + entity_name = "thermostat_evo Heating" + device_model = "HmIP-eTRV-E" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["thermostat_evo"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "33" + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.4) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ha_state.state == "40" + + +async def test_hmip_thermostat_evo_temperature(hass, default_mock_hap_factory): + """Test HomematicipTemperatureSensor.""" + entity_id = "sensor.thermostat_evo_temperature" + entity_name = "thermostat_evo Temperature" + device_model = "HmIP-eTRV-E" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["thermostat_evo"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "18.7" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 0.7) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 0.7 + + async def test_hmip_power_sensor(hass, default_mock_hap_factory): """Test HomematicipPowerSensor.""" entity_id = "sensor.flur_oben_power" diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index 1617db35458..c9a04c55dae 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -2,7 +2,8 @@ import json from unittest.mock import AsyncMock, patch -from homewizard_energy.models import Data, Device, State +from homewizard_energy.features import Features +from homewizard_energy.models import Data, Device, State, System import pytest from homeassistant.components.homewizard.const import DOMAIN @@ -37,26 +38,32 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture def mock_homewizardenergy(): - """Return a mocked P1 meter.""" + """Return a mocked all-feature device.""" with patch( "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", ) as device: client = device.return_value + client.features = AsyncMock(return_value=Features("HWE-SKT", "3.01")) client.device = AsyncMock( - return_value=Device.from_dict( + side_effect=lambda: Device.from_dict( json.loads(load_fixture("homewizard/device.json")) ) ) client.data = AsyncMock( - return_value=Data.from_dict( + side_effect=lambda: Data.from_dict( json.loads(load_fixture("homewizard/data.json")) ) ) client.state = AsyncMock( - return_value=State.from_dict( + side_effect=lambda: State.from_dict( json.loads(load_fixture("homewizard/state.json")) ) ) + client.system = AsyncMock( + side_effect=lambda: System.from_dict( + json.loads(load_fixture("homewizard/system.json")) + ) + ) yield device diff --git a/tests/components/homewizard/fixtures/system.json b/tests/components/homewizard/fixtures/system.json new file mode 100644 index 00000000000..362491b3519 --- /dev/null +++ b/tests/components/homewizard/fixtures/system.json @@ -0,0 +1,3 @@ +{ + "cloud_enabled": true +} diff --git a/tests/components/homewizard/generator.py b/tests/components/homewizard/generator.py index 0f94580ad84..dff1a4462d3 100644 --- a/tests/components/homewizard/generator.py +++ b/tests/components/homewizard/generator.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock +from homewizard_energy.features import Features from homewizard_energy.models import Device @@ -10,6 +11,7 @@ def get_mock_device( host="1.2.3.4", product_name="P1 meter", product_type="HWE-P1", + firmware_version="1.00", ): """Return a mock bridge.""" mock_device = AsyncMock() @@ -21,11 +23,15 @@ def get_mock_device( product_type=product_type, serial=serial, api_version="V1", - firmware_version="1.00", + firmware_version=firmware_version, ) ) mock_device.data = AsyncMock(return_value=None) mock_device.state = AsyncMock(return_value=None) + mock_device.system = AsyncMock(return_value=None) + mock_device.features = AsyncMock( + return_value=Features(product_type, firmware_version) + ) mock_device.close = AsyncMock() diff --git a/tests/components/homewizard/test_button.py b/tests/components/homewizard/test_button.py new file mode 100644 index 00000000000..79c6fe3c4a9 --- /dev/null +++ b/tests/components/homewizard/test_button.py @@ -0,0 +1,91 @@ +"""Test the identify button for HomeWizard.""" +from unittest.mock import patch + +from homeassistant.components import button +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er + +from .generator import get_mock_device + + +async def test_identify_button_entity_not_loaded_when_not_available( + hass, mock_config_entry_data, mock_config_entry +): + """Does not load button when device has no support for it.""" + + api = get_mock_device(product_type="HWE-P1") + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("button.product_name_aabbccddeeff_identify") is None + + +async def test_identify_button_is_loaded( + hass, mock_config_entry_data, mock_config_entry +): + """Loads button when device has support.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("button.product_name_aabbccddeeff_identify") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Identify" + ) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("button.product_name_aabbccddeeff_identify") + assert entry + assert entry.unique_id == "aabbccddeeff_identify" + + +async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config_entry): + """Test the creation and values of the Litter-Robot button.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("button.product_name_aabbccddeeff_identify").state + == STATE_UNKNOWN + ) + + assert api.identify.call_count == 0 + await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {"entity_id": "button.product_name_aabbccddeeff_identify"}, + blocking=True, + ) + assert api.identify.call_count == 1 diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index 899bfb5fb2f..ae703c58cfd 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -45,5 +45,6 @@ async def test_diagnostics( "total_liter_m3": 1234.567, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, + "system": {"cloud_enabled": True}, }, } diff --git a/tests/components/homewizard/test_number.py b/tests/components/homewizard/test_number.py new file mode 100644 index 00000000000..9538fd3cef9 --- /dev/null +++ b/tests/components/homewizard/test_number.py @@ -0,0 +1,141 @@ +"""Test the update coordinator for HomeWizard.""" + +from unittest.mock import AsyncMock, patch + +from homewizard_energy.models import State + +from homeassistant.components import number +from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.helpers import entity_registry as er + +from .generator import get_mock_device + + +async def test_number_entity_not_loaded_when_not_available( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity does not load number when brightness is not available.""" + + api = get_mock_device() + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("number.product_name_aabbccddeeff_status_light_brightness") + is None + ) + + +async def test_number_loads_entities(hass, mock_config_entry_data, mock_config_entry): + """Test entity does load number when brightness is available.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("number.product_name_aabbccddeeff_status_light_brightness") + assert state + assert state.state == "100" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Status light brightness" + ) + + entry = entity_registry.async_get( + "number.product_name_aabbccddeeff_status_light_brightness" + ) + assert entry + assert entry.unique_id == "aabbccddeeff_status_light_brightness" + assert not entry.disabled + + +async def test_brightness_level_set(hass, mock_config_entry_data, mock_config_entry): + """Test entity turns sets light level.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + def state_set(brightness): + api.state = AsyncMock(return_value=State.from_dict({"brightness": brightness})) + + api.state_set = AsyncMock(side_effect=state_set) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get( + "number.product_name_aabbccddeeff_status_light_brightness" + ).state + == "100" + ) + + # Set level halfway + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 50, + }, + blocking=True, + ) + + await hass.async_block_till_done() + assert ( + hass.states.get( + "number.product_name_aabbccddeeff_status_light_brightness" + ).state + == "50" + ) + assert len(api.state_set.mock_calls) == 1 + + # Turn off level + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 0, + }, + blocking=True, + ) + + await hass.async_block_till_done() + assert ( + hass.states.get( + "number.product_name_aabbccddeeff_status_light_brightness" + ).state + == "0" + ) + assert len(api.state_set.mock_calls) == 2 diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index 64fb5a56909..a964d548dd3 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from homewizard_energy.models import State +from homewizard_energy.models import State, System from homeassistant.components import switch from homeassistant.components.switch import SwitchDeviceClass @@ -286,3 +286,63 @@ async def test_switch_lock_sets_power_on_unavailable( == STATE_OFF ) assert len(api.state_set.mock_calls) == 2 + + +async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config_entry): + """Test entity turns switch on and off.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + api.system = AsyncMock(return_value=System.from_dict({"cloud_enabled": False})) + + def system_set(cloud_enabled): + api.system = AsyncMock( + return_value=System.from_dict({"cloud_enabled": cloud_enabled}) + ) + + api.system_set = AsyncMock(side_effect=system_set) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("switch.product_name_aabbccddeeff_cloud_connection").state + == STATE_OFF + ) + + # Enable cloud + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(api.system_set.mock_calls) == 1 + assert ( + hass.states.get("switch.product_name_aabbccddeeff_cloud_connection").state + == STATE_ON + ) + + # Disable cloud + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert ( + hass.states.get("switch.product_name_aabbccddeeff_cloud_connection").state + == STATE_OFF + ) + assert len(api.system_set.mock_calls) == 2 diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 1614555c493..b77986441ed 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -93,6 +93,7 @@ class TestHtml5Notify: def test_dismissing_message(self, mock_wp): """Test dismissing message.""" hass = MagicMock() + mock_wp().send().status_code = 201 data = {"device": SUBSCRIPTION_1} @@ -104,15 +105,13 @@ class TestHtml5Notify: service.dismiss(target=["device", "non_existing"], data={"tag": "test"}) - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1["subscription"] - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] # Call to send - payload = json.loads(mock_wp.mock_calls[1][1][0]) + payload = json.loads(mock_wp.mock_calls[3][1][0]) assert payload["dismiss"] is True assert payload["tag"] == "test" @@ -121,6 +120,7 @@ class TestHtml5Notify: def test_sending_message(self, mock_wp): """Test sending message.""" hass = MagicMock() + mock_wp().send().status_code = 201 data = {"device": SUBSCRIPTION_1} @@ -134,15 +134,13 @@ class TestHtml5Notify: "Hello", target=["device", "non_existing"], data={"icon": "beer.png"} ) - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1["subscription"] - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] # Call to send - payload = json.loads(mock_wp.mock_calls[1][1][0]) + payload = json.loads(mock_wp.mock_calls[3][1][0]) assert payload["body"] == "Hello" assert payload["icon"] == "beer.png" @@ -151,6 +149,7 @@ class TestHtml5Notify: def test_gcm_key_include(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() + mock_wp().send().status_code = 201 data = {"chrome": SUBSCRIPTION_1, "firefox": SUBSCRIPTION_2} @@ -167,21 +166,18 @@ class TestHtml5Notify: assert len(mock_wp.mock_calls) == 6 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1["subscription"] - assert mock_wp.mock_calls[3][1][0] == SUBSCRIPTION_2["subscription"] - - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" - assert mock_wp.mock_calls[5][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] + assert mock_wp.mock_calls[4][1][0] == SUBSCRIPTION_2["subscription"] # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[1][2]["gcm_key"] is not None - assert mock_wp.mock_calls[4][2]["gcm_key"] is None + assert mock_wp.mock_calls[3][2]["gcm_key"] is not None + assert mock_wp.mock_calls[5][2]["gcm_key"] is None @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_key_include(self, mock_wp): """Test if the FCM header is included.""" hass = MagicMock() + mock_wp().send().status_code = 201 data = {"chrome": SUBSCRIPTION_5} @@ -193,20 +189,18 @@ class TestHtml5Notify: service.send_message("Hello", target=["chrome"]) - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_5["subscription"] - - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[1][2]["headers"]["Authorization"] is not None + assert mock_wp.mock_calls[3][2]["headers"]["Authorization"] is not None @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_send_with_unknown_priority(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() + mock_wp().send().status_code = 201 data = {"chrome": SUBSCRIPTION_5} @@ -218,20 +212,18 @@ class TestHtml5Notify: service.send_message("Hello", target=["chrome"], priority="undefined") - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_5["subscription"] - - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_no_targets(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() + mock_wp().send().status_code = 201 data = {"chrome": SUBSCRIPTION_5} @@ -243,20 +235,18 @@ class TestHtml5Notify: service.send_message("Hello") - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_5["subscription"] - - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_additional_data(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() + mock_wp().send().status_code = 201 data = {"chrome": SUBSCRIPTION_5} @@ -268,21 +258,18 @@ class TestHtml5Notify: service.send_message("Hello", data={"mykey": "myvalue"}) - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_5["subscription"] - - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" def test_create_vapid_withoutvapid(): """Test creating empty vapid.""" resp = html5.create_vapid_headers( - vapid_email=None, vapid_private_key=None, subscription_info=None + vapid_email=None, vapid_private_key=None, subscription_info=None, timestamp=None ) assert resp is None @@ -478,6 +465,7 @@ async def test_callback_view_with_jwt(hass, hass_client): client = await mock_client(hass, hass_client, registrations) with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: + mock_wp().send().status_code = 201 await hass.services.async_call( "notify", "notify", @@ -485,15 +473,13 @@ async def test_callback_view_with_jwt(hass, hass_client): blocking=True, ) - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_1["subscription"] - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] # Call to send - push_payload = json.loads(mock_wp.mock_calls[1][1][0]) + push_payload = json.loads(mock_wp.mock_calls[3][1][0]) assert push_payload["body"] == "Hello" assert push_payload["icon"] == "beer.png" @@ -514,6 +500,7 @@ async def test_send_fcm_without_targets(hass, hass_client): registrations = {"device": SUBSCRIPTION_5} await mock_client(hass, hass_client, registrations) with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: + mock_wp().send().status_code = 201 await hass.services.async_call( "notify", "notify", @@ -521,12 +508,10 @@ async def test_send_fcm_without_targets(hass, hass_client): blocking=True, ) - assert len(mock_wp.mock_calls) == 3 + assert len(mock_wp.mock_calls) == 4 # WebPusher constructor - assert mock_wp.mock_calls[0][1][0] == SUBSCRIPTION_5["subscription"] - # Third mock_call checks the status_code of the response. - assert mock_wp.mock_calls[2][0] == "().send().status_code.__eq__" + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] async def test_send_fcm_expired(hass, hass_client): diff --git a/tests/components/http/conftest.py b/tests/components/http/conftest.py index c796ec50b51..ed2c78bafd7 100644 --- a/tests/components/http/conftest.py +++ b/tests/components/http/conftest.py @@ -3,6 +3,6 @@ import pytest @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 599df194195..8de081c840a 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -49,12 +49,12 @@ async def mock_handler(request): @pytest.fixture -def client(loop, aiohttp_client): +def client(event_loop, aiohttp_client): """Fixture to set up a web.Application.""" app = web.Application() setup_cors(app, [TRUSTED_ORIGIN]) app["allow_configured_cors"](app.router.add_get("/", mock_handler)) - return loop.run_until_complete(aiohttp_client(app)) + return event_loop.run_until_complete(aiohttp_client(app)) async def test_cors_requests(client): diff --git a/tests/components/ibeacon/__init__.py b/tests/components/ibeacon/__init__.py index 56d5eb78467..50636ee9d48 100644 --- a/tests/components/ibeacon/__init__.py +++ b/tests/components/ibeacon/__init__.py @@ -1,4 +1,6 @@ """Tests for the ibeacon integration.""" +from typing import Any + from bleak.backends.device import BLEDevice from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo @@ -115,3 +117,18 @@ FEASY_BEACON_SERVICE_INFO_2 = BluetoothServiceInfo( ], source="local", ) + + +def bluetooth_service_info_replace( + info: BluetoothServiceInfo, **kwargs: Any +) -> BluetoothServiceInfo: + """Replace attributes of a BluetoothServiceInfoBleak.""" + return BluetoothServiceInfo( + address=kwargs.get("address", info.address), + name=kwargs.get("name", info.name), + rssi=kwargs.get("rssi", info.rssi), + manufacturer_data=kwargs.get("manufacturer_data", info.manufacturer_data), + service_data=kwargs.get("service_data", info.service_data), + service_uuids=kwargs.get("service_uuids", info.service_uuids), + source=kwargs.get("source", info.source), + ) diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index 6acbf5569f8..89bc932f7a0 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -1,7 +1,6 @@ """Test the ibeacon sensors.""" -from dataclasses import replace from datetime import timedelta import time @@ -19,6 +18,7 @@ from . import ( BLUECHARM_BEACON_SERVICE_INFO_DBUS, TESLA_TRANSIENT, TESLA_TRANSIENT_BLE_DEVICE, + bluetooth_service_info_replace as replace, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -145,16 +145,17 @@ async def test_ignore_default_name(hass): assert len(hass.states.async_entity_ids()) == before_entity_count -async def test_rotating_major_minor_and_mac(hass): +async def test_rotating_major_minor_and_mac_with_name(hass): """Test the different uuid, major, minor from many addresses removes all associated entities.""" entry = MockConfigEntry( domain=DOMAIN, ) entry.add_to_hass(hass) - before_entity_count = len(hass.states.async_entity_ids("device_tracker")) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + before_entity_count = len(hass.states.async_entity_ids("device_tracker")) + for i in range(100): service_info = BluetoothServiceInfo( name="BlueCharm_177999", @@ -186,9 +187,10 @@ async def test_rotating_major_minor_and_mac_no_name(hass): ) entry.add_to_hass(hass) - before_entity_count = len(hass.states.async_entity_ids("device_tracker")) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + before_entity_count = len(hass.states.async_entity_ids("device_tracker")) + for i in range(51): service_info = BluetoothServiceInfo( name=f"AA:BB:CC:DD:EE:{i:02X}", diff --git a/tests/components/ibeacon/test_device_tracker.py b/tests/components/ibeacon/test_device_tracker.py index 26eb3dc671c..2f86ccb9042 100644 --- a/tests/components/ibeacon/test_device_tracker.py +++ b/tests/components/ibeacon/test_device_tracker.py @@ -1,7 +1,6 @@ """Test the ibeacon device trackers.""" -from dataclasses import replace from datetime import timedelta import time from unittest.mock import patch @@ -31,6 +30,7 @@ from . import ( BEACON_RANDOM_ADDRESS_SERVICE_INFO, BLUECHARM_BEACON_SERVICE_INFO, BLUECHARM_BLE_DEVICE, + bluetooth_service_info_replace as replace, ) from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/ibeacon/test_sensor.py b/tests/components/ibeacon/test_sensor.py index 671172efe93..f6a2ab51430 100644 --- a/tests/components/ibeacon/test_sensor.py +++ b/tests/components/ibeacon/test_sensor.py @@ -1,7 +1,6 @@ """Test the ibeacon sensors.""" -from dataclasses import replace from datetime import timedelta import pytest @@ -24,6 +23,7 @@ from . import ( FEASY_BEACON_SERVICE_INFO_1, FEASY_BEACON_SERVICE_INFO_2, NO_NAME_BEACON_SERVICE_INFO, + bluetooth_service_info_replace as replace, ) from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 9953df041ae..37f84fb971f 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -15,7 +15,7 @@ from tests.common import assert_setup_component, async_capture_events @pytest.fixture -def aiohttp_unused_port(loop, aiohttp_unused_port, socket_enabled): +def aiohttp_unused_port(event_loop, aiohttp_unused_port, socket_enabled): """Return aiohttp_unused_port and allow opening sockets.""" return aiohttp_unused_port diff --git a/tests/components/keymitt_ble/test_config_flow.py b/tests/components/keymitt_ble/test_config_flow.py index 81e6a0be8e7..7729f71f08c 100644 --- a/tests/components/keymitt_ble/test_config_flow.py +++ b/tests/components/keymitt_ble/test_config_flow.py @@ -1,6 +1,6 @@ """Test the MicroBot config flow.""" -from unittest.mock import ANY, patch +from unittest.mock import ANY, AsyncMock, patch from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS @@ -9,7 +9,6 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( SERVICE_INFO, USER_INPUT, - MockMicroBotApiClient, MockMicroBotApiClientFail, patch_async_setup_entry, ) @@ -19,6 +18,13 @@ from tests.common import MockConfigEntry DOMAIN = "keymitt_ble" +def patch_microbot_api(): + """Patch MicroBot API.""" + return patch( + "homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient", AsyncMock + ) + + async def test_bluetooth_discovery(hass): """Test discovery via bluetooth with a valid device.""" result = await hass.config_entries.flow.async_init( @@ -29,7 +35,7 @@ async def test_bluetooth_discovery(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" - with patch_async_setup_entry() as mock_setup_entry: + with patch_async_setup_entry() as mock_setup_entry, patch_microbot_api(): result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -51,13 +57,14 @@ async def test_bluetooth_discovery_already_setup(hass): unique_id="aa:bb:cc:dd:ee:ff", ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_BLUETOOTH}, - data=SERVICE_INFO, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + with patch_microbot_api(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" async def test_user_setup(hass): @@ -74,19 +81,18 @@ async def test_user_setup(hass): assert result["step_id"] == "init" assert result["errors"] == {} - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - USER_INPUT, - ) + with patch_microbot_api(): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "link" assert result2["errors"] is None - with patch( - "homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient", - MockMicroBotApiClient, - ), patch_async_setup_entry() as mock_setup_entry: + with patch_microbot_api(), patch_async_setup_entry() as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], USER_INPUT, @@ -124,7 +130,7 @@ async def test_user_setup_already_configured(hass): async def test_user_no_devices(hass): """Test the user initiated form with valid mac.""" - with patch( + with patch_microbot_api(), patch( "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", return_value=[], ): @@ -138,7 +144,7 @@ async def test_user_no_devices(hass): async def test_no_link(hass): """Test the user initiated form with invalid response.""" - with patch( + with patch_microbot_api(), patch( "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", return_value=[SERVICE_INFO], ): @@ -149,10 +155,11 @@ async def test_no_link(hass): assert result["step_id"] == "init" assert result["errors"] == {} - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - USER_INPUT, - ) + with patch_microbot_api(): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "link" diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 23e04b672ba..b6735df3624 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -23,6 +23,7 @@ from homeassistant.components.knx.const import ( CONF_KNX_MCAST_PORT, CONF_KNX_RATE_LIMIT, CONF_KNX_STATE_UPDATER, + DEFAULT_ROUTING_IA, DOMAIN as KNX_DOMAIN, ) from homeassistant.core import HomeAssistant @@ -224,7 +225,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, }, ) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 30b8aa537a6..55f7b2a8891 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -1,26 +1,22 @@ """Test the KNX config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest -from xknx.exceptions.exception import InvalidSignature -from xknx.io import DEFAULT_MCAST_GRP +from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration +from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.io.gateway_scanner import GatewayDescriptor from homeassistant import config_entries from homeassistant.components.knx.config_flow import ( - CONF_DEFAULT_LOCAL_IP, CONF_KNX_GATEWAY, - CONF_KNX_LABEL_TUNNELING_TCP, - CONF_KNX_LABEL_TUNNELING_TCP_SECURE, - CONF_KNX_LABEL_TUNNELING_UDP, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, CONF_KNX_TUNNELING_TYPE, DEFAULT_ENTRY_DATA, - get_knx_tunneling_type, + OPTION_MANUAL_TUNNEL, ) from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, + CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_KNXKEY_FILENAME, CONF_KNX_KNXKEY_PASSWORD, @@ -30,6 +26,9 @@ from homeassistant.components.knx.const import ( CONF_KNX_RATE_LIMIT, CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_ROUTING_BACKBONE_KEY, + CONF_KNX_ROUTING_SECURE, + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE, CONF_KNX_SECURE_DEVICE_AUTHENTICATION, CONF_KNX_SECURE_USER_ID, CONF_KNX_SECURE_USER_PASSWORD, @@ -41,16 +40,28 @@ from homeassistant.components.knx.const import ( ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry +@pytest.fixture(name="knx_setup", autouse=True) +def fixture_knx_setup(): + """Mock KNX entry setup.""" + with patch("homeassistant.components.knx.async_setup", return_value=True), patch( + "homeassistant.components.knx.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + yield mock_async_setup_entry + + def _gateway_descriptor( - ip: str, port: int, supports_tunnelling_tcp: bool = False + ip: str, + port: int, + supports_tunnelling_tcp: bool = False, + requires_secure: bool = False, ) -> GatewayDescriptor: """Get mock gw descriptor.""" - return GatewayDescriptor( + descriptor = GatewayDescriptor( name="Test", ip_addr=ip, port=port, @@ -60,6 +71,27 @@ def _gateway_descriptor( supports_tunnelling=True, supports_tunnelling_tcp=supports_tunnelling_tcp, ) + descriptor.tunnelling_requires_secure = requires_secure + descriptor.routing_requires_secure = requires_secure + return descriptor + + +class GatewayScannerMock: + """Mock GatewayScanner.""" + + def __init__(self, gateways=None): + """Initialize GatewayScannerMock.""" + # Key is a HPAI instance in xknx, but not used in HA anyway. + self.found_gateways = ( + {f"{gateway.ip_addr}:{gateway.port}": gateway for gateway in gateways} + if gateways + else {} + ) + + async def async_scan(self): + """Mock async generator.""" + for gateway in self.found_gateways: + yield gateway async def test_user_single_instance(hass): @@ -73,15 +105,19 @@ async def test_user_single_instance(hass): assert result["reason"] == "single_instance_allowed" -async def test_routing_setup(hass: HomeAssistant) -> None: +@patch( + "homeassistant.components.knx.config_flow.GatewayScanner", + return_value=GatewayScannerMock(), +) +async def test_routing_setup( + gateway_scanner_mock, hass: HomeAssistant, knx_setup +) -> None: """Test routing setup.""" - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [] - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -89,51 +125,49 @@ async def test_routing_setup(hass: HomeAssistant) -> None: CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_router_discovered"} - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", - }, - ) - await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == CONF_KNX_ROUTING.capitalize() - assert result3["data"] == { - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_LOCAL_IP: None, CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", - } - - assert len(mock_setup_entry.mock_calls) == 1 + }, + ) + await hass.async_block_till_done() + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Routing as 1.1.110" + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_LOCAL_IP: None, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + } + knx_setup.assert_called_once() -async def test_routing_setup_advanced(hass: HomeAssistant) -> None: +@patch( + "homeassistant.components.knx.config_flow.GatewayScanner", + return_value=GatewayScannerMock(), +) +async def test_routing_setup_advanced( + gateway_scanner_mock, hass: HomeAssistant, knx_setup +) -> None: """Test routing setup with advanced options.""" - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [] - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_USER, - "show_advanced_options": True, - }, - ) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_USER, + "show_advanced_options": True, + }, + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -141,10 +175,9 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_router_discovered"} # invalid user input result_invalid_input = await hass.config_entries.flow.async_configure( @@ -156,42 +189,195 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: CONF_KNX_LOCAL_IP: "no_local_ip", }, ) - await hass.async_block_till_done() assert result_invalid_input["type"] == FlowResultType.FORM assert result_invalid_input["step_id"] == "routing" assert result_invalid_input["errors"] == { CONF_KNX_MCAST_GRP: "invalid_ip_address", CONF_KNX_INDIVIDUAL_ADDRESS: "invalid_individual_address", CONF_KNX_LOCAL_IP: "invalid_ip_address", + "base": "no_router_discovered", } # valid user input + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Routing as 1.1.110" + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + } + knx_setup.assert_called_once() + + +@patch( + "homeassistant.components.knx.config_flow.GatewayScanner", + return_value=GatewayScannerMock(), +) +async def test_routing_secure_manual_setup( + gateway_scanner_mock, hass: HomeAssistant, knx_setup +) -> None: + """Test routing secure setup with manual key config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "routing" + assert result2["errors"] == {"base": "no_router_discovered"} + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3671, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123", + CONF_KNX_ROUTING_SECURE: True, + }, + ) + assert result3["type"] == FlowResultType.MENU + assert result3["step_id"] == "secure_key_source" + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {"next_step_id": "secure_routing_manual"}, + ) + assert result4["type"] == FlowResultType.FORM + assert result4["step_id"] == "secure_routing_manual" + assert not result4["errors"] + + result_invalid_key1 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + CONF_KNX_ROUTING_BACKBONE_KEY: "xxaacc44bbaacc44bbaacc44bbaaccyy", # invalid hex string + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, + }, + ) + assert result_invalid_key1["type"] == FlowResultType.FORM + assert result_invalid_key1["step_id"] == "secure_routing_manual" + assert result_invalid_key1["errors"] == {"backbone_key": "invalid_backbone_key"} + + result_invalid_key2 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44", # invalid length + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, + }, + ) + assert result_invalid_key2["type"] == FlowResultType.FORM + assert result_invalid_key2["step_id"] == "secure_routing_manual" + assert result_invalid_key2["errors"] == {"backbone_key": "invalid_backbone_key"} + + secure_routing_manual = await hass.config_entries.flow.async_configure( + result_invalid_key2["flow_id"], + { + CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44", + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, + }, + ) + await hass.async_block_till_done() + assert secure_routing_manual["type"] == FlowResultType.CREATE_ENTRY + assert secure_routing_manual["title"] == "Secure Routing as 0.0.123" + assert secure_routing_manual["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING_SECURE, + CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44", + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123", + } + knx_setup.assert_called_once() + + +@patch( + "homeassistant.components.knx.config_flow.GatewayScanner", + return_value=GatewayScannerMock(), +) +async def test_routing_secure_keyfile( + gateway_scanner_mock, hass: HomeAssistant, knx_setup +) -> None: + """Test routing secure setup with keyfile.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "routing" + assert result2["errors"] == {"base": "no_router_discovered"} + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: 3671, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123", + CONF_KNX_ROUTING_SECURE: True, + }, + ) + assert result3["type"] == FlowResultType.MENU + assert result3["step_id"] == "secure_key_source" + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {"next_step_id": "secure_knxkeys"}, + ) + assert result4["type"] == FlowResultType.FORM + assert result4["step_id"] == "secure_knxkeys" + assert not result4["errors"] + with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], + "homeassistant.components.knx.config_flow.load_keyring", return_value=True + ): + routing_secure_knxkeys = await hass.config_entries.flow.async_configure( + result4["flow_id"], { - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", - CONF_KNX_LOCAL_IP: "192.168.1.112", + CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == CONF_KNX_ROUTING.capitalize() - assert result3["data"] == { - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_LOCAL_IP: "192.168.1.112", - CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", - } - - assert len(mock_setup_entry.mock_calls) == 1 + assert routing_secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123" + assert routing_secure_knxkeys["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING_SECURE, + CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + CONF_KNX_ROUTING_BACKBONE_KEY: None, + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: None, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, + CONF_KNX_SECURE_USER_ID: None, + CONF_KNX_SECURE_USER_PASSWORD: None, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.123", + } + knx_setup.assert_called_once() @pytest.mark.parametrize( @@ -199,66 +385,74 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: [ ( { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, + CONF_KNX_ROUTE_BACK: False, }, { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", CONF_KNX_ROUTE_BACK: False, CONF_KNX_LOCAL_IP: None, }, ), ( { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_TCP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, + CONF_KNX_ROUTE_BACK: False, }, { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", CONF_KNX_ROUTE_BACK: False, CONF_KNX_LOCAL_IP: None, }, ), ( { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, + CONF_KNX_ROUTE_BACK: True, }, { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", CONF_KNX_ROUTE_BACK: True, CONF_KNX_LOCAL_IP: None, }, ), ], ) -async def test_tunneling_setup( - hass: HomeAssistant, user_input, config_entry_data +@patch( + "homeassistant.components.knx.config_flow.GatewayScanner", + return_value=GatewayScannerMock(), +) +async def test_tunneling_setup_manual( + _gateway_scanner_mock, + hass: HomeAssistant, + knx_setup, + user_input, + config_entry_data, ) -> None: - """Test tunneling if only one gateway is found.""" - gateway = _gateway_descriptor("192.168.0.1", 3675, True) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] + """Test tunneling if no gateway was found found (or `manual` option was chosen).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -266,41 +460,186 @@ async def test_tunneling_setup( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_tunnel_discovered"} with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + "homeassistant.components.knx.config_flow.request_description", + return_value=_gateway_descriptor( + user_input[CONF_HOST], + user_input[CONF_PORT], + supports_tunnelling_tcp=( + user_input[CONF_KNX_TUNNELING_TYPE] == CONF_KNX_TUNNELING_TCP + ), + ), + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "Tunneling @ 192.168.0.1" - assert result3["data"] == config_entry_data - - assert len(mock_setup_entry.mock_calls) == 1 + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Tunneling @ 192.168.0.1" + assert result3["data"] == config_entry_data + knx_setup.assert_called_once() -async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: - """Test tunneling if only one gateway is found.""" - gateway = _gateway_descriptor("192.168.0.2", 3675) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_USER, - "show_advanced_options": True, +@patch( + "homeassistant.components.knx.config_flow.GatewayScanner", + return_value=GatewayScannerMock(), +) +async def test_tunneling_setup_manual_request_description_error( + _gateway_scanner_mock, + hass: HomeAssistant, + knx_setup, +) -> None: + """Test tunneling if no gateway was found found (or `manual` option was chosen).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + assert result["step_id"] == "manual_tunnel" + assert result["errors"] == {"base": "no_tunnel_discovered"} + + # TCP configured but not supported by gateway + with patch( + "homeassistant.components.knx.config_flow.request_description", + return_value=_gateway_descriptor( + "192.168.0.1", + 3671, + supports_tunnelling_tcp=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3671, }, ) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] + assert result["step_id"] == "manual_tunnel" + assert result["errors"] == { + "base": "no_tunnel_discovered", + "tunneling_type": "unsupported_tunnel_type", + } + # TCP configured but Secure required by gateway + with patch( + "homeassistant.components.knx.config_flow.request_description", + return_value=_gateway_descriptor( + "192.168.0.1", + 3671, + supports_tunnelling_tcp=True, + requires_secure=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3671, + }, + ) + assert result["step_id"] == "manual_tunnel" + assert result["errors"] == { + "base": "no_tunnel_discovered", + "tunneling_type": "unsupported_tunnel_type", + } + # Secure configured but not enabled on gateway + with patch( + "homeassistant.components.knx.config_flow.request_description", + return_value=_gateway_descriptor( + "192.168.0.1", + 3671, + supports_tunnelling_tcp=True, + requires_secure=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3671, + }, + ) + assert result["step_id"] == "manual_tunnel" + assert result["errors"] == { + "base": "no_tunnel_discovered", + "tunneling_type": "unsupported_tunnel_type", + } + # No connection to gateway + with patch( + "homeassistant.components.knx.config_flow.request_description", + side_effect=CommunicationError(""), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3671, + }, + ) + assert result["step_id"] == "manual_tunnel" + assert result["errors"] == {"base": "cannot_connect"} + # OK configuration + with patch( + "homeassistant.components.knx.config_flow.request_description", + return_value=_gateway_descriptor( + "192.168.0.1", + 3671, + supports_tunnelling_tcp=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3671, + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Tunneling @ 192.168.0.1" + assert result["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3671, + } + knx_setup.assert_called_once() + + +@patch( + "homeassistant.components.knx.config_flow.GatewayScanner", + return_value=GatewayScannerMock(), +) +@patch( + "homeassistant.components.knx.config_flow.request_description", + return_value=_gateway_descriptor("192.168.0.2", 3675), +) +async def test_tunneling_setup_for_local_ip( + _request_description_mock, _gateway_scanner_mock, hass: HomeAssistant, knx_setup +) -> None: + """Test tunneling if only one gateway is found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_USER, + "show_advanced_options": True, + }, + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -308,76 +647,78 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_tunnel_discovered"} # invalid host ip address result_invalid_host = await hass.config_entries.flow.async_configure( result2["flow_id"], { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: DEFAULT_MCAST_GRP, # multicast addresses are invalid CONF_PORT: 3675, CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) - await hass.async_block_till_done() assert result_invalid_host["type"] == FlowResultType.FORM assert result_invalid_host["step_id"] == "manual_tunnel" - assert result_invalid_host["errors"] == {CONF_HOST: "invalid_ip_address"} + assert result_invalid_host["errors"] == { + CONF_HOST: "invalid_ip_address", + "base": "no_tunnel_discovered", + } # invalid local ip address result_invalid_local = await hass.config_entries.flow.async_configure( result2["flow_id"], { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.2", CONF_PORT: 3675, CONF_KNX_LOCAL_IP: "asdf", }, ) - await hass.async_block_till_done() assert result_invalid_local["type"] == FlowResultType.FORM assert result_invalid_local["step_id"] == "manual_tunnel" - assert result_invalid_local["errors"] == {CONF_KNX_LOCAL_IP: "invalid_ip_address"} + assert result_invalid_local["errors"] == { + CONF_KNX_LOCAL_IP: "invalid_ip_address", + "base": "no_tunnel_discovered", + } # valid user input - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, - CONF_HOST: "192.168.0.2", - CONF_PORT: 3675, - CONF_KNX_LOCAL_IP: "192.168.1.112", - }, - ) - await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "Tunneling @ 192.168.0.2" - assert result3["data"] == { - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.2", CONF_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_ROUTE_BACK: False, CONF_KNX_LOCAL_IP: "192.168.1.112", - } - - assert len(mock_setup_entry.mock_calls) == 1 + }, + ) + await hass.async_block_till_done() + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Tunneling @ 192.168.0.2" + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: "192.168.1.112", + } + knx_setup.assert_called_once() -async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) -> None: - """Test tunneling if only one gateway is found.""" +async def test_tunneling_setup_for_multiple_found_gateways( + hass: HomeAssistant, knx_setup +) -> None: + """Test tunneling if multiple gateways are found.""" gateway = _gateway_descriptor("192.168.0.1", 3675) gateway2 = _gateway_descriptor("192.168.1.100", 3675) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway, gateway2] + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway, gateway2]) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -390,50 +731,46 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - await hass.async_block_till_done() assert tunnel_flow["type"] == FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] - manual_tunnel = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( tunnel_flow["flow_id"], {CONF_KNX_GATEWAY: str(gateway)}, ) await hass.async_block_till_done() - assert manual_tunnel["type"] == FlowResultType.FORM - assert manual_tunnel["step_id"] == "manual_tunnel" + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, + } + knx_setup.assert_called_once() + +@pytest.mark.parametrize( + "gateway", + [ + _gateway_descriptor("192.168.0.1", 3675), + _gateway_descriptor("192.168.0.1", 3675, supports_tunnelling_tcp=True), + _gateway_descriptor( + "192.168.0.1", 3675, supports_tunnelling_tcp=True, requires_secure=True + ), + ], +) +async def test_manual_tunnel_step_with_found_gateway( + hass: HomeAssistant, gateway +) -> None: + """Test manual tunnel if gateway was found and tunneling is selected.""" with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - manual_tunnel_flow = await hass.config_entries.flow.async_configure( - manual_tunnel["flow_id"], - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, - CONF_HOST: "192.168.0.1", - CONF_PORT: 3675, - }, - ) - await hass.async_block_till_done() - assert manual_tunnel_flow["type"] == FlowResultType.CREATE_ENTRY - assert manual_tunnel_flow["data"] == { - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_HOST: "192.168.0.1", - CONF_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_ROUTE_BACK: False, - CONF_KNX_LOCAL_IP: None, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: - """Test manual tunnel if no gateway is found and tunneling is selected.""" - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [] + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway]) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -446,49 +783,65 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - await hass.async_block_till_done() assert tunnel_flow["type"] == FlowResultType.FORM - assert tunnel_flow["step_id"] == "manual_tunnel" + assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] + manual_tunnel_flow = await hass.config_entries.flow.async_configure( + tunnel_flow["flow_id"], + { + CONF_KNX_GATEWAY: OPTION_MANUAL_TUNNEL, + }, + ) + assert manual_tunnel_flow["type"] == FlowResultType.FORM + assert manual_tunnel_flow["step_id"] == "manual_tunnel" + assert not manual_tunnel_flow["errors"] -async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> None: + +async def test_form_with_automatic_connection_handling( + hass: HomeAssistant, knx_setup +) -> None: """Test we get the form.""" - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [_gateway_descriptor("192.168.0.1", 3675)] + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock( + [_gateway_descriptor("192.168.0.1", 3675)] + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert not result["errors"] - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - }, - ) - await hass.async_block_till_done() - + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + }, + ) + await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, } - - assert len(mock_setup_entry.mock_calls) == 1 + knx_setup.assert_called_once() -async def _get_menu_step(hass: HomeAssistant) -> None: - """Test ip secure manuel.""" - gateway = _gateway_descriptor("192.168.0.1", 3675, True) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] +async def _get_menu_step(hass: HomeAssistant) -> FlowResult: + """Return flow in secure_tunnelling menu step.""" + gateway = _gateway_descriptor( + "192.168.0.1", + 3675, + supports_tunnelling_tcp=True, + requires_secure=True, + ) + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway]) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -501,68 +854,116 @@ async def _get_menu_step(hass: HomeAssistant) -> None: CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "manual_tunnel" + assert result2["step_id"] == "tunnel" assert not result2["errors"] result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], + {CONF_KNX_GATEWAY: str(gateway)}, + ) + assert result3["type"] == FlowResultType.MENU + assert result3["step_id"] == "secure_key_source" + return result3 + + +@patch( + "homeassistant.components.knx.config_flow.request_description", + return_value=_gateway_descriptor( + "192.168.0.1", + 3675, + supports_tunnelling_tcp=True, + requires_secure=True, + ), +) +async def test_get_secure_menu_step_manual_tunnelling( + _request_description_mock, + hass: HomeAssistant, +): + """Test flow reaches secure_tunnellinn menu step from manual tunnelling configuration.""" + gateway = _gateway_descriptor( + "192.168.0.1", + 3675, + supports_tunnelling_tcp=True, + requires_secure=True, + ) + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway]) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_TCP_SECURE, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "tunnel" + assert not result2["errors"] + + manual_tunnel_flow = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_KNX_GATEWAY: OPTION_MANUAL_TUNNEL, + }, + ) + + result3 = await hass.config_entries.flow.async_configure( + manual_tunnel_flow["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, }, ) - await hass.async_block_till_done() assert result3["type"] == FlowResultType.MENU - assert result3["step_id"] == "secure_tunneling" - return result3 + assert result3["step_id"] == "secure_key_source" -async def test_configure_secure_manual(hass: HomeAssistant): - """Test configure secure manual.""" +async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup): + """Test configure tunnelling secure keys manually.""" menu_step = await _get_menu_step(hass) result = await hass.config_entries.flow.async_configure( menu_step["flow_id"], - {"next_step_id": "secure_manual"}, + {"next_step_id": "secure_tunnel_manual"}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "secure_manual" + assert result["step_id"] == "secure_tunnel_manual" assert not result["errors"] - with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - secure_manual = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_KNX_SECURE_USER_ID: 2, - CONF_KNX_SECURE_USER_PASSWORD: "password", - CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", - }, - ) - await hass.async_block_till_done() - assert secure_manual["type"] == FlowResultType.CREATE_ENTRY - assert secure_manual["data"] == { - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + secure_tunnel_manual = await hass.config_entries.flow.async_configure( + result["flow_id"], + { CONF_KNX_SECURE_USER_ID: 2, CONF_KNX_SECURE_USER_PASSWORD: "password", CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", - CONF_HOST: "192.168.0.1", - CONF_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_ROUTE_BACK: False, - CONF_KNX_LOCAL_IP: None, - } - - assert len(mock_setup_entry.mock_calls) == 1 + }, + ) + await hass.async_block_till_done() + assert secure_tunnel_manual["type"] == FlowResultType.CREATE_ENTRY + assert secure_tunnel_manual["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_KNX_SECURE_USER_ID: 2, + CONF_KNX_SECURE_USER_PASSWORD: "password", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, + } + knx_setup.assert_called_once() -async def test_configure_secure_knxkeys(hass: HomeAssistant): +async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup): """Test configure secure knxkeys.""" menu_step = await _get_menu_step(hass) @@ -575,10 +976,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): assert not result["errors"] with patch( - "homeassistant.components.knx.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.knx.config_flow.load_key_ring", return_value=True + "homeassistant.components.knx.config_flow.load_keyring", return_value=True ): secure_knxkeys = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -588,20 +986,24 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY - assert secure_knxkeys["data"] == { - **DEFAULT_ENTRY_DATA, - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, - CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", - CONF_KNX_KNXKEY_PASSWORD: "password", - CONF_HOST: "192.168.0.1", - CONF_PORT: 3675, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_ROUTE_BACK: False, - CONF_KNX_LOCAL_IP: None, - } - - assert len(mock_setup_entry.mock_calls) == 1 + assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert secure_knxkeys["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + CONF_KNX_ROUTING_BACKBONE_KEY: None, + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: None, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, + CONF_KNX_SECURE_USER_ID: None, + CONF_KNX_SECURE_USER_PASSWORD: None, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, + } + knx_setup.assert_called_once() async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): @@ -617,7 +1019,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): assert not result["errors"] with patch( - "homeassistant.components.knx.config_flow.load_key_ring", + "homeassistant.components.knx.config_flow.load_keyring", side_effect=FileNotFoundError(), ): secure_knxkeys = await hass.config_entries.flow.async_configure( @@ -627,7 +1029,6 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - await hass.async_block_till_done() assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "file_not_found" @@ -646,8 +1047,8 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): assert not result["errors"] with patch( - "homeassistant.components.knx.config_flow.load_key_ring", - side_effect=InvalidSignature(), + "homeassistant.components.knx.config_flow.load_keyring", + side_effect=InvalidSecureConfiguration(), ): secure_knxkeys = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -656,271 +1057,194 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - await hass.async_block_till_done() assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature" -async def test_options_flow( - hass: HomeAssistant, mock_config_entry: MockConfigEntry +async def test_options_flow_connection_type( + hass: HomeAssistant, knx_setup, mock_config_entry: MockConfigEntry ) -> None: - """Test options config flow.""" + """Test options flow changing interface.""" mock_config_entry.add_to_hass(hass) - gateway = _gateway_descriptor("192.168.0.1", 3675) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] - result = await hass.config_entries.options.async_init( - mock_config_entry.entry_id + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + hass.data[DOMAIN] = Mock() # GatewayScanner uses running XKNX() in options flow + menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway]) + result = await hass.config_entries.options.async_configure( + menu_step["flow_id"], + {"next_step_id": "connection_type"}, ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert "flow_id" in result - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - }, - ) - - await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY - assert not result2.get("data") - - assert mock_config_entry.data == { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_HOST: "", - CONF_KNX_LOCAL_IP: None, - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - } - - -@pytest.mark.parametrize( - "user_input,config_entry_data", - [ - ( - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - CONF_KNX_LOCAL_IP: None, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - CONF_KNX_ROUTE_BACK: True, - }, - ), - ( - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - CONF_KNX_LOCAL_IP: None, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - CONF_KNX_ROUTE_BACK: False, - }, - ), - ( - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_TCP, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - CONF_KNX_LOCAL_IP: None, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - CONF_KNX_ROUTE_BACK: False, - }, - ), - ], -) -async def test_tunneling_options_flow( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - user_input, - config_entry_data, -) -> None: - """Test options flow for tunneling.""" - mock_config_entry.add_to_hass(hass) - - gateway = _gateway_descriptor("192.168.0.1", 3675) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] - result = await hass.config_entries.options.async_init( - mock_config_entry.entry_id - ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert "flow_id" in result + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "connection_type" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, }, ) - - assert result2.get("type") == FlowResultType.FORM - assert not result2.get("data") - assert "flow_id" in result2 + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "tunnel" result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input=user_input, + user_input={ + CONF_KNX_GATEWAY: str(gateway), + }, ) - await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY - assert not result3.get("data") - - assert mock_config_entry.data == config_entry_data + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert not result3["data"] + assert mock_config_entry.data == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 0, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_ROUTE_BACK: False, + } + knx_setup.assert_called_once() -@pytest.mark.parametrize( - "user_input,config_entry_data", - [ - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: "192.168.1.112", - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_HOST: "", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: "192.168.1.112", - }, - ), - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: CONF_DEFAULT_LOCAL_IP, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_HOST: "", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: None, - }, - ), - ], -) -async def test_advanced_options( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - user_input, - config_entry_data, +async def test_options_flow_secure_manual_to_keyfile( + hass: HomeAssistant, knx_setup ) -> None: - """Test options config flow.""" + """Test options flow changing secure credential source.""" + mock_config_entry = MockConfigEntry( + title="KNX", + domain="knx", + data={ + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_KNX_SECURE_USER_ID: 2, + CONF_KNX_SECURE_USER_PASSWORD: "password", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", + CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "invalid_password", + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, + }, + ) + gateway = _gateway_descriptor( + "192.168.0.1", + 3675, + supports_tunnelling_tcp=True, + requires_secure=True, + ) + mock_config_entry.add_to_hass(hass) - - gateway = _gateway_descriptor("192.168.0.1", 3675) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] - result = await hass.config_entries.options.async_init( - mock_config_entry.entry_id, context={"show_advanced_options": True} + await hass.config_entries.async_setup(mock_config_entry.entry_id) + menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway]) + result = await hass.config_entries.options.async_configure( + menu_step["flow_id"], + {"next_step_id": "connection_type"}, ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert "flow_id" in result + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "connection_type" result2 = await hass.config_entries.options.async_configure( result["flow_id"], - user_input=user_input, + user_input={ + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "tunnel" + assert not result2["errors"] + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + {CONF_KNX_GATEWAY: str(gateway)}, + ) + assert result3["type"] == FlowResultType.MENU + assert result3["step_id"] == "secure_key_source" + + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + {"next_step_id": "secure_knxkeys"}, + ) + assert result4["type"] == FlowResultType.FORM + assert result4["step_id"] == "secure_knxkeys" + assert not result4["errors"] + + with patch( + "homeassistant.components.knx.config_flow.load_keyring", return_value=True + ): + secure_knxkeys = await hass.config_entries.options.async_configure( + result4["flow_id"], + { + CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + }, + ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY - assert not result2.get("data") - - assert mock_config_entry.data == config_entry_data + assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert mock_config_entry.data == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, + CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, + CONF_KNX_SECURE_USER_ID: None, + CONF_KNX_SECURE_USER_PASSWORD: None, + CONF_KNX_ROUTING_BACKBONE_KEY: None, + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: None, + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_LOCAL_IP: None, + } + knx_setup.assert_called_once() -@pytest.mark.parametrize( - "config_entry_data,result", - [ - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_ROUTE_BACK: False, - }, - CONF_KNX_LABEL_TUNNELING_UDP, - ), - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_ROUTE_BACK: True, - }, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - ), - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, - CONF_KNX_ROUTE_BACK: False, - }, - CONF_KNX_LABEL_TUNNELING_TCP, - ), - ], -) -async def test_get_knx_tunneling_type( - config_entry_data, - result, +async def test_options_communication_settings( + hass: HomeAssistant, knx_setup, mock_config_entry: MockConfigEntry ) -> None: - """Test converting config entry data to tunneling type for config flow.""" - assert get_knx_tunneling_type(config_entry_data) == result + """Test options flow changing communication settings.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + result = await hass.config_entries.options.async_configure( + menu_step["flow_id"], + {"next_step_id": "communication_settings"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "communication_settings" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_RATE_LIMIT: 40, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert not result2.get("data") + assert mock_config_entry.data == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_RATE_LIMIT: 40, + } + knx_setup.assert_called_once() diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index 5f4dd01bcb6..130f1f70f35 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -2,7 +2,6 @@ from unittest.mock import patch from aiohttp import ClientSession -from xknx import XKNX from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from homeassistant.components.knx.const import ( @@ -15,9 +14,11 @@ from homeassistant.components.knx.const import ( CONF_KNX_MCAST_GRP, CONF_KNX_MCAST_PORT, CONF_KNX_RATE_LIMIT, + CONF_KNX_ROUTING_BACKBONE_KEY, CONF_KNX_SECURE_DEVICE_AUTHENTICATION, CONF_KNX_SECURE_USER_PASSWORD, CONF_KNX_STATE_UPDATER, + DEFAULT_ROUTING_IA, DOMAIN as KNX_DOMAIN, ) from homeassistant.core import HomeAssistant @@ -45,10 +46,10 @@ async def test_diagnostics( ) == { "config_entry_data": { "connection_type": "automatic", - "individual_address": "15.15.250", + "individual_address": "0.0.240", "multicast_group": "224.0.23.12", "multicast_port": 3671, - "rate_limit": 20, + "rate_limit": 0, "state_updater": True, }, "configuration_error": None, @@ -77,10 +78,10 @@ async def test_diagnostic_config_error( ) == { "config_entry_data": { "connection_type": "automatic", - "individual_address": "15.15.250", + "individual_address": "0.0.240", "multicast_group": "224.0.23.12", "multicast_port": 3671, - "rate_limit": 20, + "rate_limit": 0, "state_updater": True, }, "configuration_error": "extra keys not allowed @ data['knx']['wrong_key']", @@ -103,10 +104,11 @@ async def test_diagnostic_redact( CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, CONF_KNX_KNXKEY_PASSWORD: "password", CONF_KNX_SECURE_USER_PASSWORD: "user_password", CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_authentication", + CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44", }, ) knx: KNXTestKit = KNXTestKit(hass, mock_config_entry) @@ -120,14 +122,15 @@ async def test_diagnostic_redact( ) == { "config_entry_data": { "connection_type": "automatic", - "individual_address": "15.15.250", + "individual_address": "0.0.240", "multicast_group": "224.0.23.12", "multicast_port": 3671, - "rate_limit": 20, + "rate_limit": 0, "state_updater": True, "knxkeys_password": "**REDACTED**", "user_password": "**REDACTED**", "device_authentication": "**REDACTED**", + "backbone_key": "**REDACTED**", }, "configuration_error": None, "configuration_yaml": None, diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 82ddf73f1ea..3a8a42fdf98 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -1,6 +1,5 @@ """Test KNX init.""" import pytest -from xknx import XKNX from xknx.io import ( DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT, @@ -9,6 +8,7 @@ from xknx.io import ( SecureConfig, ) +from homeassistant.components.knx.config_flow import DEFAULT_ROUTING_IA from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, @@ -23,6 +23,9 @@ from homeassistant.components.knx.const import ( CONF_KNX_RATE_LIMIT, CONF_KNX_ROUTE_BACK, CONF_KNX_ROUTING, + CONF_KNX_ROUTING_BACKBONE_KEY, + CONF_KNX_ROUTING_SECURE, + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE, CONF_KNX_SECURE_DEVICE_AUTHENTICATION, CONF_KNX_SECURE_USER_ID, CONF_KNX_SECURE_USER_PASSWORD, @@ -51,7 +54,7 @@ from tests.common import MockConfigEntry CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, }, ConnectionConfig(threaded=True), ), @@ -63,10 +66,13 @@ from tests.common import MockConfigEntry CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, }, ConnectionConfig( connection_type=ConnectionType.ROUTING, + individual_address=DEFAULT_ROUTING_IA, + multicast_group=DEFAULT_MCAST_GRP, + multicast_port=DEFAULT_MCAST_PORT, local_ip="192.168.1.1", threaded=True, ), @@ -82,7 +88,7 @@ from tests.common import MockConfigEntry CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, }, ConnectionConfig( connection_type=ConnectionType.TUNNELING, @@ -103,7 +109,7 @@ from tests.common import MockConfigEntry CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, }, ConnectionConfig( connection_type=ConnectionType.TUNNELING_TCP, @@ -122,7 +128,7 @@ from tests.common import MockConfigEntry CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys", CONF_KNX_KNXKEY_PASSWORD: "password", }, @@ -146,7 +152,7 @@ from tests.common import MockConfigEntry CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, CONF_KNX_SECURE_USER_ID: 2, CONF_KNX_SECURE_USER_PASSWORD: "password", CONF_KNX_SECURE_DEVICE_AUTHENTICATION: "device_auth", @@ -164,6 +170,31 @@ from tests.common import MockConfigEntry threaded=True, ), ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING_SECURE, + CONF_KNX_LOCAL_IP: "192.168.1.1", + CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_INDIVIDUAL_ADDRESS: DEFAULT_ROUTING_IA, + CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44", + CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, + }, + ConnectionConfig( + connection_type=ConnectionType.ROUTING_SECURE, + individual_address=DEFAULT_ROUTING_IA, + multicast_group=DEFAULT_MCAST_GRP, + multicast_port=DEFAULT_MCAST_PORT, + secure_config=SecureConfig( + backbone_key="bbaacc44bbaacc44bbaacc44bbaacc44", + latency_ms=2000, + ), + local_ip="192.168.1.1", + threaded=True, + ), + ), ], ) async def test_init_connection_handling( diff --git a/tests/components/knx/test_text.py b/tests/components/knx/test_text.py new file mode 100644 index 00000000000..0f5169054b3 --- /dev/null +++ b/tests/components/knx/test_text.py @@ -0,0 +1,100 @@ +"""Test KNX number.""" +from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS +from homeassistant.components.knx.schema import TextSchema +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, State + +from .conftest import KNXTestKit + +from tests.common import mock_restore_cache + + +async def test_text(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX text.""" + test_address = "1/1/1" + await knx.setup_integration( + { + TextSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + } + } + ) + # set value + await hass.services.async_call( + "text", + "set_value", + {"entity_id": "text.test", "value": "hello world"}, + blocking=True, + ) + await knx.assert_write( + test_address, + ( + 0x68, + 0x65, + 0x6C, + 0x6C, + 0x6F, + 0x20, + 0x77, + 0x6F, + 0x72, + 0x6C, + 0x64, + 0x0, + 0x0, + 0x0, + ), + ) + state = hass.states.get("text.test") + assert state.state == "hello world" + + # update from KNX + await knx.receive_write( + test_address, + (0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0), + ) + state = hass.states.get("text.test") + assert state.state == "hallo" + + +async def test_text_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX text with passive_address, restoring state and respond_to_read.""" + test_address = "1/1/1" + test_passive_address = "3/3/3" + + fake_state = State("text.test", "test test") + mock_restore_cache(hass, (fake_state,)) + + await knx.setup_integration( + { + TextSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: [test_address, test_passive_address], + CONF_RESPOND_TO_READ: True, + } + } + ) + # restored state - doesn't send telegram + state = hass.states.get("text.test") + assert state.state == "test test" + await knx.assert_telegram_count(0) + + # respond with restored state + await knx.receive_read(test_address) + await knx.assert_response( + test_address, + (0x74, 0x65, 0x73, 0x74, 0x20, 0x74, 0x65, 0x73, 0x74, 0x0, 0x0, 0x0, 0x0, 0x0), + ) + + # don't respond to passive address + await knx.receive_read(test_passive_address) + await knx.assert_no_telegram() + + # update from KNX passive address + await knx.receive_write( + test_passive_address, + (0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0), + ) + state = hass.states.get("text.test") + assert state.state == "hallo" diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index 5b8b07583c5..9fb215e2d8a 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -68,7 +68,6 @@ class MockConnection: async def connect(self): """Mock connect.""" - pass @property def connected(self): @@ -82,7 +81,6 @@ class MockConnection: async def close(self): """Mock close.""" - pass @property def server(self): @@ -99,7 +97,6 @@ class MockWSConnection: async def connect(self): """Mock connect.""" - pass @property def connected(self): @@ -113,7 +110,6 @@ class MockWSConnection: async def close(self): """Mock close.""" - pass @property def server(self): diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index 9200a9b3d23..57638868647 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -1,7 +1,8 @@ """Test the Landis + Gyr Heat Meter config flow.""" from dataclasses import dataclass -from unittest.mock import MagicMock, patch +from unittest.mock import patch +import serial import serial.tools.list_ports from homeassistant import config_entries @@ -9,6 +10,10 @@ from homeassistant.components.landisgyr_heat_meter import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + +API_HEAT_METER_SERVICE = "homeassistant.components.landisgyr_heat_meter.config_flow.ultraheat_api.HeatMeterService" + def mock_serial_port(): """Mock of a serial port.""" @@ -17,6 +22,8 @@ def mock_serial_port(): port.manufacturer = "Virtual serial port" port.device = "/dev/ttyUSB1234" port.description = "Some serial port" + port.pid = 9876 + port.vid = 5678 return port @@ -29,7 +36,7 @@ class MockUltraheatRead: device_number: str -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: """Test manual entry.""" @@ -67,7 +74,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: } -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) @patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: """Test select from list entry.""" @@ -94,11 +101,11 @@ async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> No } -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: """Test manual entry fails.""" - mock_heat_meter().read.side_effect = Exception + mock_heat_meter().read.side_effect = serial.serialutil.SerialException result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -128,12 +135,12 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) @patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: """Test select from list entry fails.""" - mock_heat_meter().read.side_effect = Exception + mock_heat_meter().read.side_effect = serial.serialutil.SerialException port = mock_serial_port() result = await hass.config_entries.flow.async_init( @@ -151,77 +158,36 @@ async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) assert result["errors"] == {"base": "cannot_connect"} -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) @patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) -async def test_get_serial_by_id_realpath( +async def test_already_configured( mock_port, mock_heat_meter, hass: HomeAssistant ) -> None: - """Test getting the serial path name.""" + """Test we abort if the Heat Meter is already configured.""" + # create and add existing entry + entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="123456789", 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() + + # run flow and see if it aborts mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") port = mock_serial_port() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - scandir = [MagicMock(), MagicMock()] - scandir[0].path = "/dev/ttyUSB1234" - scandir[0].is_symlink.return_value = True - scandir[1].path = "/dev/ttyUSB5678" - scandir[1].is_symlink.return_value = True - - with patch("os.path") as path: - with patch("os.scandir", return_value=scandir): - path.isdir.return_value = True - path.realpath.side_effect = ["/dev/ttyUSB1234", "/dev/ttyUSB5678"] - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device": port.device} - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "LUGCUH50" - assert result["data"] == { - "device": port.device, - "model": "LUGCUH50", - "device_number": "123456789", - } - - -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") -@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) -async def test_get_serial_by_id_dev_path( - mock_port, mock_heat_meter, hass: HomeAssistant -) -> None: - """Test getting the serial path name with no realpath result.""" - - mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") - port = mock_serial_port() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - scandir = [MagicMock()] - scandir[0].path.return_value = "/dev/serial/by-id/USB5678" - scandir[0].is_symlink.return_value = True - - with patch("os.path") as path: - with patch("os.scandir", return_value=scandir): - path.isdir.return_value = True - path.realpath.side_effect = ["/dev/ttyUSB5678"] - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device": port.device} - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "LUGCUH50" - assert result["data"] == { - "device": port.device, - "model": "LUGCUH50", - "device_number": "123456789", - } + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/landisgyr_heat_meter/test_init.py b/tests/components/landisgyr_heat_meter/test_init.py index b3630fc4872..6e300ec1332 100644 --- a/tests/components/landisgyr_heat_meter/test_init.py +++ b/tests/components/landisgyr_heat_meter/test_init.py @@ -1,22 +1,78 @@ """Test the Landis + Gyr Heat Meter init.""" -from homeassistant.const import CONF_DEVICE +from unittest.mock import patch + +from homeassistant.components.landisgyr_heat_meter.const import ( + DOMAIN as LANDISGYR_HEAT_METER_DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry +API_HEAT_METER_SERVICE = ( + "homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService" +) -async def test_unload_entry(hass): + +@patch(API_HEAT_METER_SERVICE) +async def test_unload_entry(_, hass): """Test removing config entry.""" - entry = MockConfigEntry( + mock_entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "12345", + } + mock_entry = MockConfigEntry( domain="landisgyr_heat_meter", title="LUGCUH50", - data={CONF_DEVICE: "/dev/1234"}, + entry_id="987654321", + data=mock_entry_data, ) + mock_entry.add_to_hass(hass) - entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(entry.entry_id) + assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert "landisgyr_heat_meter" in hass.config.components - assert await hass.config_entries.async_remove(entry.entry_id) + assert await hass.config_entries.async_remove(mock_entry.entry_id) + + +@patch(API_HEAT_METER_SERVICE) +async def test_migrate_entry(_, hass): + """Test successful migration of entry data from version 1 to 2.""" + + mock_entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "12345", + } + mock_entry = MockConfigEntry( + domain="landisgyr_heat_meter", + title="LUGCUH50", + entry_id="987654321", + data=mock_entry_data, + ) + assert mock_entry.data == mock_entry_data + assert mock_entry.version == 1 + + mock_entry.add_to_hass(hass) + + # Create entity entry to migrate to new unique ID + registry = er.async_get(hass) + registry.async_get_or_create( + SENSOR_DOMAIN, + LANDISGYR_HEAT_METER_DOMAIN, + "landisgyr_heat_meter_987654321_measuring_range_m3ph", + suggested_object_id="heat_meter_measuring_range", + config_entry=mock_entry, + ) + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert "landisgyr_heat_meter" in hass.config.components + + # Check if entity unique id is migrated successfully + assert mock_entry.version == 2 + entity = registry.async_get("sensor.heat_meter_measuring_range") + assert entity.unique_id == "12345_measuring_range_m3ph" diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index 1a068093d0e..cbaca71e52f 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -42,7 +42,7 @@ class MockHeatMeterResponse: meter_date_time: datetime.datetime -@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService") async def test_create_sensors(mock_heat_meter, hass): """Test sensor.""" entry_data = { @@ -107,7 +107,7 @@ async def test_create_sensors(mock_heat_meter, hass): assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC -@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService") async def test_restore_state(mock_heat_meter, hass): """Test sensor restore state.""" # Home assistant is not running yet @@ -177,7 +177,6 @@ async def test_restore_state(mock_heat_meter, hass): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) - await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() # restore from cache @@ -195,6 +194,5 @@ async def test_restore_state(mock_heat_meter, hass): state = hass.states.get("sensor.heat_meter_device_number") assert state - print("STATE IS: ", state) assert state.state == "devicenr_789" assert state.attributes.get(ATTR_STATE_CLASS) is None diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index cbb37f94dc4..d20b54e738f 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -30,7 +30,6 @@ class MockUser: def get_image(self): """Get mock image.""" - pass def get_recent_tracks(self, limit): """Get mock recent tracks.""" diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 6f084f939d8..0a62f8d317c 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -56,7 +56,7 @@ async def test_step_import_existing_host(hass): mock_data.update({CONF_SK_NUM_TRIES: 3, CONF_DIM_MODE: 50}) mock_entry = MockConfigEntry(domain=DOMAIN, data=mock_data) mock_entry.add_to_hass(hass) - # Inititalize a config flow with different data but same host address + # Initialize a config flow with different data but same host address with patch("pypck.connection.PchkConnectionManager.async_connect"): imported_data = IMPORT_DATA.copy() result = await hass.config_entries.flow.async_init( diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py index 8bcf817cbba..2ac1912723b 100644 --- a/tests/components/lg_soundbar/test_config_flow.py +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -1,5 +1,10 @@ """Test the lg_soundbar config flow.""" -from unittest.mock import DEFAULT, MagicMock, Mock, call, patch +from __future__ import annotations + +from collections.abc import Callable +import socket +from typing import Any +from unittest.mock import DEFAULT, patch from homeassistant import config_entries from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN @@ -8,6 +13,43 @@ from homeassistant.const import CONF_HOST, CONF_PORT from tests.common import MockConfigEntry +def setup_mock_temescal( + hass, mock_temescal, mac_info_dev=None, product_info=None, info=None +): + """Set up a mock of the temescal object to craft our expected responses.""" + tmock = mock_temescal.temescal + instance = tmock.return_value + + def create_temescal_response(msg: str, data: dict | None = None) -> dict[str, Any]: + response: dict[str, Any] = {"msg": msg} + if data is not None: + response["data"] = data + return response + + def temescal_side_effect( + addr: str, port: int, callback: Callable[[dict[str, Any]], None] + ): + mac_info_response = create_temescal_response( + msg="MAC_INFO_DEV", data=mac_info_dev + ) + product_info_response = create_temescal_response( + msg="PRODUCT_INFO", data=product_info + ) + info_response = create_temescal_response(msg="SPK_LIST_VIEW_INFO", data=info) + + instance.get_mac_info.side_effect = lambda: hass.add_job( + callback, mac_info_response + ) + instance.get_product_info.side_effect = lambda: hass.add_job( + callback, product_info_response + ) + instance.get_info.side_effect = lambda: hass.add_job(callback, info_response) + + return DEFAULT + + tmock.side_effect = temescal_side_effect + + async def test_form(hass): """Test we get the form.""" @@ -18,14 +60,16 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal", - return_value=MagicMock(), - ), patch( - "homeassistant.components.lg_soundbar.config_flow.test_connect", - return_value={"uuid": "uuid", "name": "name"}, - ), patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, patch( "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True ) as mock_setup_entry: + setup_mock_temescal( + hass=hass, + mock_temescal=mock_temescal, + mac_info_dev={"s_uuid": "uuid"}, + info={"s_user_name": "name"}, + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -36,6 +80,7 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == "name" + assert result2["result"].unique_id == "uuid" assert result2["data"] == { CONF_HOST: "1.1.1.1", CONF_PORT: DEFAULT_PORT, @@ -43,8 +88,8 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_uuid_missing_from_mac_info(hass): - """Test we get the form, but uuid is missing from the initial get_mac_info function call.""" +async def test_form_mac_info_response_empty(hass): + """Test we get the form, but response from the initial get_mac_info function call is empty.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -53,23 +98,16 @@ async def test_form_uuid_missing_from_mac_info(hass): assert result["errors"] == {} with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + "homeassistant.components.lg_soundbar.config_flow.temescal" ) as mock_temescal, patch( "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True ) as mock_setup_entry: - tmock = mock_temescal.temescal - tmock.return_value = Mock() - instance = tmock.return_value - - def temescal_side_effect(addr, port, callback): - product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} - instance.get_product_info.side_effect = lambda: callback(product_info) - info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} - instance.get_info.side_effect = lambda: callback(info) - return DEFAULT - - tmock.side_effect = temescal_side_effect - + setup_mock_temescal( + hass=hass, + mock_temescal=mock_temescal, + mac_info_dev={"s_uuid": "uuid"}, + info={"s_user_name": "name"}, + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -80,6 +118,7 @@ async def test_form_uuid_missing_from_mac_info(hass): assert result2["type"] == "create_entry" assert result2["title"] == "name" + assert result2["result"].unique_id == "uuid" assert result2["data"] == { CONF_HOST: "1.1.1.1", CONF_PORT: DEFAULT_PORT, @@ -99,35 +138,18 @@ async def test_form_uuid_present_in_both_functions_uuid_q_empty(hass): assert result["type"] == "form" assert result["errors"] == {} - mock_uuid_q = MagicMock() - mock_name_q = MagicMock() - with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + "homeassistant.components.lg_soundbar.config_flow.temescal" ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.config_flow.Queue", - return_value=MagicMock(), - ) as mock_q, patch( "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True ) as mock_setup_entry: - mock_q.side_effect = [mock_uuid_q, mock_name_q] - mock_uuid_q.empty.return_value = True - mock_uuid_q.get.return_value = "uuid" - mock_name_q.get.return_value = "name" - tmock = mock_temescal.temescal - tmock.return_value = Mock() - instance = tmock.return_value - - def temescal_side_effect(addr, port, callback): - mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} - instance.get_mac_info.side_effect = lambda: callback(mac_info) - product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} - instance.get_product_info.side_effect = lambda: callback(product_info) - info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} - instance.get_info.side_effect = lambda: callback(info) - return DEFAULT - - tmock.side_effect = temescal_side_effect + setup_mock_temescal( + hass=hass, + mock_temescal=mock_temescal, + mac_info_dev={"s_uuid": "uuid"}, + product_info={"s_uuid": "uuid"}, + info={"s_user_name": "name"}, + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -139,14 +161,12 @@ async def test_form_uuid_present_in_both_functions_uuid_q_empty(hass): assert result2["type"] == "create_entry" assert result2["title"] == "name" + assert result2["result"].unique_id == "uuid" assert result2["data"] == { CONF_HOST: "1.1.1.1", CONF_PORT: DEFAULT_PORT, } assert len(mock_setup_entry.mock_calls) == 1 - mock_uuid_q.empty.assert_called_once() - mock_uuid_q.put_nowait.has_calls([call("uuid"), call("uuid")]) - mock_uuid_q.get.assert_called_once() async def test_form_uuid_present_in_both_functions_uuid_q_not_empty(hass): @@ -161,33 +181,21 @@ async def test_form_uuid_present_in_both_functions_uuid_q_not_empty(hass): assert result["type"] == "form" assert result["errors"] == {} - mock_uuid_q = MagicMock() - mock_name_q = MagicMock() - with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.config_flow.Queue", - return_value=MagicMock(), - ) as mock_q, patch( "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True ) as mock_setup_entry: - mock_q.side_effect = [mock_uuid_q, mock_name_q] - mock_uuid_q.empty.return_value = False - mock_uuid_q.get.return_value = "uuid" - mock_name_q.get.return_value = "name" - tmock = mock_temescal.temescal - tmock.return_value = Mock() - instance = tmock.return_value - - def temescal_side_effect(addr, port, callback): - mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} - instance.get_mac_info.side_effect = lambda: callback(mac_info) - info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} - instance.get_info.side_effect = lambda: callback(info) - return DEFAULT - - tmock.side_effect = temescal_side_effect + setup_mock_temescal( + hass=hass, + mock_temescal=mock_temescal, + mac_info_dev={"s_uuid": "uuid"}, + product_info={"s_uuid": "uuid"}, + info={"s_user_name": "name"}, + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -199,26 +207,196 @@ async def test_form_uuid_present_in_both_functions_uuid_q_not_empty(hass): assert result2["type"] == "create_entry" assert result2["title"] == "name" + assert result2["result"].unique_id == "uuid" assert result2["data"] == { CONF_HOST: "1.1.1.1", CONF_PORT: DEFAULT_PORT, } assert len(mock_setup_entry.mock_calls) == 1 - mock_uuid_q.empty.assert_called_once() - mock_uuid_q.put_nowait.assert_called_once() - mock_uuid_q.get.assert_called_once() -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" +async def test_form_uuid_missing_from_mac_info(hass): + """Test we get the form, but uuid is missing from the initial get_mac_info function call.""" + + 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.lg_soundbar.config_flow.temescal" + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + setup_mock_temescal( + hass=hass, + mock_temescal=mock_temescal, + product_info={"s_uuid": "uuid"}, + info={"s_user_name": "name"}, + ) + + 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"] == "create_entry" + assert result2["title"] == "name" + assert result2["result"].unique_id == "uuid" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_uuid_not_provided_by_api(hass): + """Test we get the form, but uuid is missing from the all API messages.""" + + 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.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + setup_mock_temescal( + hass=hass, + mock_temescal=mock_temescal, + product_info={"i_model_no": "8", "i_model_type": 0}, + info={"s_user_name": "name"}, + ) + 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"] == "create_entry" + assert result2["title"] == "name" + assert result2["result"].unique_id is None + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_both_queues_empty(hass): + """Test we get the form, but none of the data we want is provided by the API.""" + + 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.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + setup_mock_temescal(hass=hass, mock_temescal=mock_temescal) + + 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["errors"] == {"base": "no_data"} + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_no_uuid_host_already_configured(hass): + """Test we handle if the device has no UUID and the host has already been configured.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + }, + ) + mock_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.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal: + setup_mock_temescal( + hass=hass, mock_temescal=mock_temescal, info={"s_user_name": "name"} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_form_socket_timeout(hass): + """Test we handle socket.timeout error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.lg_soundbar.config_flow.test_connect", - side_effect=ConnectionError, - ): + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal: + mock_temescal.temescal.side_effect = socket.timeout + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_os_error(hass): + """Test we handle OSError.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal: + mock_temescal.temescal.side_effect = OSError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -247,9 +425,15 @@ async def test_form_already_configured(hass): ) with patch( - "homeassistant.components.lg_soundbar.config_flow.test_connect", - return_value={"uuid": "uuid", "name": "name"}, - ): + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal: + setup_mock_temescal( + hass=hass, + mock_temescal=mock_temescal, + mac_info_dev={"s_uuid": "uuid"}, + info={"s_user_name": "name"}, + ) + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 774376e1a99..bb861c4a683 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -87,6 +87,7 @@ def _mocked_bulb() -> Light: bulb.set_reboot = Mock() bulb.try_sending = AsyncMock() bulb.set_infrared = MockLifxCommand(bulb) + bulb.get_label = MockLifxCommand(bulb) bulb.get_color = MockLifxCommand(bulb) bulb.set_power = MockLifxCommand(bulb) bulb.set_color = MockLifxCommand(bulb) diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 3bf82fce3fc..f1126d8ab7b 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -13,6 +13,8 @@ from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES from homeassistant.components.lifx.manager import ( ATTR_DIRECTION, ATTR_PALETTE, + ATTR_SATURATION_MAX, + ATTR_SATURATION_MIN, ATTR_SPEED, ATTR_THEME, SERVICE_EFFECT_COLORLOOP, @@ -1009,7 +1011,20 @@ async def test_color_light_with_temp( await hass.services.async_call( DOMAIN, SERVICE_EFFECT_COLORLOOP, - {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 50, ATTR_SATURATION_MAX: 90}, + blocking=True, + ) + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectColorloop) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT_COLORLOOP, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128, ATTR_SATURATION_MIN: 90}, blocking=True, ) start_call = mock_effect_conductor.start.mock_calls diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index a06d490cea6..86713b51489 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -12,7 +12,7 @@ import homeassistant.util.dt as dt_util from . import async_init_integration -from tests.common import async_fire_time_changed, async_mock_service +from tests.common import async_fire_time_changed_exact, async_mock_service from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ async def simulate_time(hass, mock_litejet, delta): return_value=mock_litejet.start_time + delta, ): _LOGGER.info("now=%s", dt_util.utcnow()) - async_fire_time_changed(hass, mock_litejet.start_time + delta) + async_fire_time_changed_exact(hass, mock_litejet.start_time + delta) await hass.async_block_till_done() _LOGGER.info("done with now=%s", dt_util.utcnow()) @@ -113,12 +113,12 @@ async def test_held_more_than_short(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "200"}, + "held_more_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_litejet, timedelta(seconds=0.1)) + await simulate_time(hass, mock_litejet, timedelta(seconds=1)) await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -130,13 +130,13 @@ async def test_held_more_than_long(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "200"}, + "held_more_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.3)) + await simulate_time(hass, mock_litejet, timedelta(seconds=3)) assert len(calls) == 1 assert calls[0].data["id"] == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) @@ -150,12 +150,12 @@ async def test_held_less_than_short(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_less_than": {"milliseconds": "200"}, + "held_less_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_litejet, timedelta(seconds=0.1)) + await simulate_time(hass, mock_litejet, timedelta(seconds=1)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 @@ -169,13 +169,13 @@ async def test_held_less_than_long(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_less_than": {"milliseconds": "200"}, + "held_less_than": {"milliseconds": "2000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.3)) + await simulate_time(hass, mock_litejet, timedelta(seconds=3)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -188,13 +188,13 @@ async def test_held_in_range_short(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) - await simulate_time(hass, mock_litejet, timedelta(seconds=0.05)) + await simulate_time(hass, mock_litejet, timedelta(seconds=0.5)) await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -206,14 +206,14 @@ async def test_held_in_range_just_right(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.2)) + await simulate_time(hass, mock_litejet, timedelta(seconds=2)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 1 @@ -227,14 +227,14 @@ async def test_held_in_range_long(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.4)) + await simulate_time(hass, mock_litejet, timedelta(seconds=4)) assert len(calls) == 0 await simulate_release(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 @@ -247,8 +247,8 @@ async def test_reload(hass, calls, mock_litejet): { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "100"}, - "held_less_than": {"milliseconds": "300"}, + "held_more_than": {"milliseconds": "1000"}, + "held_less_than": {"milliseconds": "3000"}, }, ) @@ -260,7 +260,7 @@ async def test_reload(hass, calls, mock_litejet): "trigger": { "platform": "litejet", "number": ENTITY_OTHER_SWITCH_NUMBER, - "held_more_than": {"milliseconds": "1000"}, + "held_more_than": {"milliseconds": "10000"}, }, "action": {"service": "test.automation"}, } @@ -275,7 +275,7 @@ async def test_reload(hass, calls, mock_litejet): await simulate_press(hass, mock_litejet, ENTITY_OTHER_SWITCH_NUMBER) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=0.5)) + await simulate_time(hass, mock_litejet, timedelta(seconds=5)) assert len(calls) == 0 - await simulate_time(hass, mock_litejet, timedelta(seconds=1.25)) + await simulate_time(hass, mock_litejet, timedelta(seconds=12.5)) assert len(calls) == 1 diff --git a/tests/components/livisi/__init__.py b/tests/components/livisi/__init__.py new file mode 100644 index 00000000000..3d28d1db708 --- /dev/null +++ b/tests/components/livisi/__init__.py @@ -0,0 +1,37 @@ +"""Tests for the LIVISI Smart Home integration.""" +from unittest.mock import patch + +from homeassistant.components.livisi.const import CONF_HOST, CONF_PASSWORD + +VALID_CONFIG = { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test", +} + +DEVICE_CONFIG = { + "serialNumber": "1234", + "controllerType": "Classic", +} + + +def mocked_livisi_login(): + """Create mock for LIVISI login.""" + return patch( + "homeassistant.components.livisi.config_flow.AioLivisi.async_set_token" + ) + + +def mocked_livisi_controller(): + """Create mock data for LIVISI controller.""" + return patch( + "homeassistant.components.livisi.config_flow.AioLivisi.async_get_controller", + return_value=DEVICE_CONFIG, + ) + + +def mocked_livisi_setup_entry(): + """Create mock for LIVISI setup entry.""" + return patch( + "homeassistant.components.livisi.async_setup_entry", + return_value=True, + ) diff --git a/tests/components/livisi/test_config_flow.py b/tests/components/livisi/test_config_flow.py new file mode 100644 index 00000000000..c9924d39b9b --- /dev/null +++ b/tests/components/livisi/test_config_flow.py @@ -0,0 +1,68 @@ +"""Test the Livisi Home Assistant config flow.""" + +from unittest.mock import patch + +from aiolivisi import errors as livisi_errors +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.livisi.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER + +from . import ( + VALID_CONFIG, + mocked_livisi_controller, + mocked_livisi_login, + mocked_livisi_setup_entry, +) + + +async def test_create_entry(hass): + """Test create LIVISI entity.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with mocked_livisi_login(), mocked_livisi_controller(), mocked_livisi_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "SHC Classic" + assert result["data"]["host"] == "1.1.1.1" + assert result["data"]["password"] == "test" + + +@pytest.mark.parametrize( + "exception,expected_reason", + [ + (livisi_errors.ShcUnreachableException(), "cannot_connect"), + (livisi_errors.IncorrectIpAddressException(), "wrong_ip_address"), + (livisi_errors.WrongCredentialException(), "wrong_password"), + ], +) +async def test_create_entity_after_login_error( + hass, exception: livisi_errors.LivisiException, expected_reason: str +): + """Test the LIVISI integration can create an entity after the user had login errors.""" + with patch( + "homeassistant.components.livisi.config_flow.AioLivisi.async_set_token", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], VALID_CONFIG + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"]["base"] == expected_reason + with mocked_livisi_login(), mocked_livisi_controller(), mocked_livisi_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=VALID_CONFIG, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/local_calendar/__init__.py b/tests/components/local_calendar/__init__.py new file mode 100644 index 00000000000..b7326472757 --- /dev/null +++ b/tests/components/local_calendar/__init__.py @@ -0,0 +1 @@ +"""Tests for the Local Calendar integration.""" diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py new file mode 100644 index 00000000000..092fcb1c1fb --- /dev/null +++ b/tests/components/local_calendar/test_calendar.py @@ -0,0 +1,707 @@ +"""Tests for calendar platform of local calendar.""" + +from collections.abc import Awaitable, Callable +import datetime +from http import HTTPStatus +from pathlib import Path +from typing import Any +from unittest.mock import patch +import urllib + +from aiohttp import ClientSession, ClientWebSocketResponse +import pytest + +from homeassistant.components.local_calendar import LocalCalendarStore +from homeassistant.components.local_calendar.const import CONF_CALENDAR_NAME, DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.template import DATE_STR_FORMAT +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry + +CALENDAR_NAME = "Light Schedule" +FRIENDLY_NAME = "Light schedule" +TEST_ENTITY = "calendar.light_schedule" + + +class FakeStore(LocalCalendarStore): + """Mock storage implementation.""" + + def __init__(self, hass: HomeAssistant, path: Path) -> None: + """Initialize FakeStore.""" + super().__init__(hass, path) + self._content = "" + + def _load(self) -> str: + """Read from calendar storage.""" + return self._content + + def _store(self, ics_content: str) -> None: + """Persist the calendar storage.""" + self._content = ics_content + + +@pytest.fixture(name="store", autouse=True) +def mock_store() -> None: + """Test cleanup, remove any media storage persisted during the test.""" + + def new_store(hass: HomeAssistant, path: Path) -> FakeStore: + return FakeStore(hass, path) + + with patch( + "homeassistant.components.local_calendar.LocalCalendarStore", new=new_store + ): + yield + + +@pytest.fixture(name="time_zone") +def mock_time_zone() -> str: + """Fixture for time zone to use in tests.""" + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + return "America/Regina" + + +@pytest.fixture(autouse=True) +def set_time_zone(hass: HomeAssistant, time_zone: str): + """Set the time zone for the tests.""" + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + hass.config.set_time_zone(time_zone) + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Fixture for mock configuration entry.""" + return MockConfigEntry(domain=DOMAIN, data={CONF_CALENDAR_NAME: CALENDAR_NAME}) + + +@pytest.fixture(name="setup_integration") +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the integration.""" + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + +GetEventsFn = Callable[[str, str], Awaitable[dict[str, Any]]] + + +@pytest.fixture(name="get_events") +def get_events_fixture( + hass_client: Callable[..., Awaitable[ClientSession]] +) -> GetEventsFn: + """Fetch calendar events from the HTTP API.""" + + async def _fetch(start: str, end: str) -> None: + client = await hass_client() + response = await client.get( + f"/api/calendars/{TEST_ENTITY}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" + ) + assert response.status == HTTPStatus.OK + return await response.json() + + return _fetch + + +def event_fields(data: dict[str, str]) -> dict[str, str]: + """Filter event API response to minimum fields.""" + return { + k: data.get(k) + for k in ["summary", "start", "end", "recurrence_id"] + if data.get(k) + } + + +class Client: + """Test client with helper methods for calendar websocket.""" + + def __init__(self, client): + """Initialize Client.""" + self.client = client + self.id = 0 + + async def cmd(self, cmd: str, payload: dict[str, Any] = None) -> dict[str, Any]: + """Send a command and receive the json result.""" + self.id += 1 + await self.client.send_json( + { + "id": self.id, + "type": f"calendar/event/{cmd}", + **(payload if payload is not None else {}), + } + ) + resp = await self.client.receive_json() + assert resp.get("id") == self.id + return resp + + async def cmd_result(self, cmd: str, payload: dict[str, Any] = None) -> Any: + """Send a command and parse the result.""" + resp = await self.cmd(cmd, payload) + assert resp.get("success") + assert resp.get("type") == "result" + return resp.get("result") + + +ClientFixture = Callable[[], Awaitable[Client]] + + +@pytest.fixture +async def ws_client( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> ClientFixture: + """Fixture for creating the test websocket client.""" + + async def create_client() -> Client: + ws_client = await hass_ws_client(hass) + return Client(ws_client) + + return create_client + + +async def test_empty_calendar( + hass: HomeAssistant, setup_integration: None, get_events: GetEventsFn +): + """Test querying the API and fetching events.""" + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert len(events) == 0 + + state = hass.states.get(TEST_ENTITY) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_OFF + assert dict(state.attributes) == { + "friendly_name": FRIENDLY_NAME, + "supported_features": 3, + } + + +async def test_api_date_time_event( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +): + """Test an event with a start/end date time.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-14T17:00:00+00:00", + "dtend": "1997-07-15T04:00:00+00:00", + }, + }, + ) + + events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + + # Time range before event + events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T16:00:00Z") + assert len(events) == 0 + # Time range after event + events = await get_events("1997-07-15T05:00:00Z", "1997-07-15T06:00:00Z") + assert len(events) == 0 + + # Overlap with event start + events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T18:00:00Z") + assert len(events) == 1 + # Overlap with event end + events = await get_events("1997-07-15T03:00:00Z", "1997-07-15T06:00:00Z") + assert len(events) == 1 + + +async def test_api_date_event( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +): + """Test an event with a start/end date all day event.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Festival International de Jazz de Montreal", + "dtstart": "2007-06-28", + "dtend": "2007-07-09", + }, + }, + ) + + events = await get_events("2007-06-20T00:00:00", "2007-07-20T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Festival International de Jazz de Montreal", + "start": {"date": "2007-06-28"}, + "end": {"date": "2007-07-09"}, + } + ] + + # Time range before event (timezone is -6) + events = await get_events("2007-06-26T00:00:00Z", "2007-06-28T01:00:00Z") + assert len(events) == 0 + # Time range after event + events = await get_events("2007-07-10T00:00:00Z", "2007-07-11T00:00:00Z") + assert len(events) == 0 + + # Overlap with event start (timezone is -6) + events = await get_events("2007-06-26T00:00:00Z", "2007-06-28T08:00:00Z") + assert len(events) == 1 + # Overlap with event end + events = await get_events("2007-07-09T00:00:00Z", "2007-07-11T00:00:00Z") + assert len(events) == 1 + + +async def test_active_event( + hass: HomeAssistant, + ws_client: ClientFixture, + setup_integration: None, +): + """Test an event with a start/end date time.""" + start = dt_util.now() - datetime.timedelta(minutes=30) + end = dt_util.now() + datetime.timedelta(minutes=30) + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Evening lights", + "dtstart": start.isoformat(), + "dtend": end.isoformat(), + }, + }, + ) + + state = hass.states.get(TEST_ENTITY) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_ON + assert dict(state.attributes) == { + "friendly_name": FRIENDLY_NAME, + "message": "Evening lights", + "all_day": False, + "description": "", + "location": "", + "start_time": start.strftime(DATE_STR_FORMAT), + "end_time": end.strftime(DATE_STR_FORMAT), + "supported_features": 3, + } + + +async def test_upcoming_event( + hass: HomeAssistant, + ws_client: ClientFixture, + setup_integration: None, +): + """Test an event with a start/end date time.""" + start = dt_util.now() + datetime.timedelta(days=1) + end = dt_util.now() + datetime.timedelta(days=1, hours=1) + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Evening lights", + "dtstart": start.isoformat(), + "dtend": end.isoformat(), + }, + }, + ) + + state = hass.states.get(TEST_ENTITY) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_OFF + assert dict(state.attributes) == { + "friendly_name": FRIENDLY_NAME, + "message": "Evening lights", + "all_day": False, + "description": "", + "location": "", + "message": "Evening lights", + "start_time": start.strftime(DATE_STR_FORMAT), + "end_time": end.strftime(DATE_STR_FORMAT), + "supported_features": 3, + } + + +async def test_recurring_event( + ws_client: ClientFixture, + setup_integration: None, + hass: HomeAssistant, + get_events: GetEventsFn, +): + """Test an event with a recurrence rule.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Monday meeting", + "dtstart": "2022-08-29T09:00:00", + "dtend": "2022-08-29T10:00:00", + "rrule": "FREQ=WEEKLY", + }, + }, + ) + + events = await get_events("2022-08-20T00:00:00", "2022-09-20T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Monday meeting", + "start": {"dateTime": "2022-08-29T09:00:00-06:00"}, + "end": {"dateTime": "2022-08-29T10:00:00-06:00"}, + "recurrence_id": "20220829T090000", + }, + { + "summary": "Monday meeting", + "start": {"dateTime": "2022-09-05T09:00:00-06:00"}, + "end": {"dateTime": "2022-09-05T10:00:00-06:00"}, + "recurrence_id": "20220905T090000", + }, + { + "summary": "Monday meeting", + "start": {"dateTime": "2022-09-12T09:00:00-06:00"}, + "end": {"dateTime": "2022-09-12T10:00:00-06:00"}, + "recurrence_id": "20220912T090000", + }, + { + "summary": "Monday meeting", + "start": {"dateTime": "2022-09-19T09:00:00-06:00"}, + "end": {"dateTime": "2022-09-19T10:00:00-06:00"}, + "recurrence_id": "20220919T090000", + }, + ] + + +async def test_websocket_delete( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +): + """Test websocket delete command.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-14T17:00:00+00:00", + "dtend": "1997-07-15T04:00:00+00:00", + }, + }, + ) + + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + uid = events[0]["uid"] + + # Delete the event + await client.cmd_result( + "delete", + { + "entity_id": TEST_ENTITY, + "uid": uid, + }, + ) + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [] + + +async def test_websocket_delete_recurring( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +): + """Test deleting a recurring event.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Morning Routine", + "dtstart": "2022-08-22T08:30:00", + "dtend": "2022-08-22T09:00:00", + "rrule": "FREQ=DAILY", + }, + }, + ) + + events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-22T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-22T09:00:00-06:00"}, + "recurrence_id": "20220822T083000", + }, + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-23T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-23T09:00:00-06:00"}, + "recurrence_id": "20220823T083000", + }, + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-24T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-24T09:00:00-06:00"}, + "recurrence_id": "20220824T083000", + }, + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-25T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-25T09:00:00-06:00"}, + "recurrence_id": "20220825T083000", + }, + ] + uid = events[0]["uid"] + assert [event["uid"] for event in events] == [uid] * 4 + + # Cancel a single instance and confirm it was removed + await client.cmd_result( + "delete", + { + "entity_id": TEST_ENTITY, + "uid": uid, + "recurrence_id": "20220824T083000", + }, + ) + events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-22T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-22T09:00:00-06:00"}, + "recurrence_id": "20220822T083000", + }, + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-23T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-23T09:00:00-06:00"}, + "recurrence_id": "20220823T083000", + }, + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-25T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-25T09:00:00-06:00"}, + "recurrence_id": "20220825T083000", + }, + ] + + # Delete all and future and confirm multiple were removed + await client.cmd_result( + "delete", + { + "entity_id": TEST_ENTITY, + "uid": uid, + "recurrence_id": "20220823T083000", + "recurrence_range": "THISANDFUTURE", + }, + ) + events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Morning Routine", + "start": {"dateTime": "2022-08-22T08:30:00-06:00"}, + "end": {"dateTime": "2022-08-22T09:00:00-06:00"}, + "recurrence_id": "20220822T083000", + }, + ] + + +@pytest.mark.parametrize( + "rrule", + [ + "FREQ=SECONDLY", + "FREQ=MINUTELY", + "FREQ=HOURLY", + "invalid", + "", + ], +) +async def test_invalid_rrule( + ws_client: ClientFixture, + setup_integration: None, + hass: HomeAssistant, + get_events: GetEventsFn, + rrule: str, +): + """Test an event with a recurrence rule.""" + client = await ws_client() + resp = await client.cmd( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Monday meeting", + "dtstart": "2022-08-29T09:00:00", + "dtend": "2022-08-29T10:00:00", + "rrule": rrule, + }, + }, + ) + assert not resp.get("success") + assert "error" in resp + assert resp.get("error").get("code") == "invalid_format" + + +@pytest.mark.parametrize( + "time_zone,event_order", + [ + ("America/Los_Angeles", ["One", "Two", "All Day Event"]), + ("America/Regina", ["One", "Two", "All Day Event"]), + ("UTC", ["One", "All Day Event", "Two"]), + ("Asia/Tokyo", ["All Day Event", "One", "Two"]), + ], +) +async def test_all_day_iter_order( + hass: HomeAssistant, + ws_client: ClientFixture, + setup_integration: None, + get_events: GetEventsFn, + event_order: list[str], +): + """Test the sort order of an all day events depending on the time zone.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "All Day Event", + "dtstart": "2022-10-08", + "dtend": "2022-10-09", + }, + }, + ) + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "One", + "dtstart": "2022-10-07T23:00:00+00:00", + "dtend": "2022-10-07T23:30:00+00:00", + }, + }, + ) + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Two", + "dtstart": "2022-10-08T01:00:00+00:00", + "dtend": "2022-10-08T02:00:00+00:00", + }, + }, + ) + + events = await get_events("2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z") + assert [event["summary"] for event in events] == event_order + + +async def test_start_end_types( + ws_client: ClientFixture, + setup_integration: None, +): + """Test a start and end with different date and date time types.""" + client = await ws_client() + result = await client.cmd( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-15", + "dtend": "1997-07-14T17:00:00+00:00", + }, + }, + ) + assert not result.get("success") + assert "error" in result + assert "code" in result.get("error") + assert result["error"]["code"] == "invalid_format" + + +async def test_end_before_start( + ws_client: ClientFixture, + setup_integration: None, +): + """Test an event with a start/end date time.""" + client = await ws_client() + result = await client.cmd( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-15T04:00:00+00:00", + "dtend": "1997-07-14T17:00:00+00:00", + }, + }, + ) + assert not result.get("success") + assert "error" in result + assert "code" in result.get("error") + assert result["error"]["code"] == "invalid_format" + + +async def test_invalid_recurrence_rule( + ws_client: ClientFixture, + setup_integration: None, +): + """Test an event with a recurrence rule.""" + client = await ws_client() + result = await client.cmd( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Monday meeting", + "dtstart": "2022-08-29T09:00:00", + "dtend": "2022-08-29T10:00:00", + "rrule": "FREQ=invalid;'", + }, + }, + ) + assert not result.get("success") + assert "error" in result + assert "code" in result.get("error") + assert result["error"]["code"] == "invalid_format" + + +async def test_invalid_date_formats( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +): + """Exercises a validation error within rfc5545 parsing in ical.""" + client = await ws_client() + result = await client.cmd( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + # Can't mix offset aware and floating dates + "dtstart": "1997-07-15T04:00:00+08:00", + "dtend": "1997-07-14T17:00:00", + }, + }, + ) + assert not result.get("success") + assert "error" in result + assert "code" in result.get("error") + assert result["error"]["code"] == "invalid_format" diff --git a/tests/components/local_calendar/test_config_flow.py b/tests/components/local_calendar/test_config_flow.py new file mode 100644 index 00000000000..25049326762 --- /dev/null +++ b/tests/components/local_calendar/test_config_flow.py @@ -0,0 +1,35 @@ +"""Test the Local Calendar config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.local_calendar.const import CONF_CALENDAR_NAME, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.local_calendar.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_CALENDAR_NAME: "My Calendar", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "My Calendar" + assert result2["data"] == { + CONF_CALENDAR_NAME: "My Calendar", + } + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 1b84e3e8552..4de150eaf7a 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -1,12 +1,15 @@ """Tests for the local_ip config_flow.""" +from __future__ import annotations + from homeassistant import data_entry_flow from homeassistant.components.local_ip.const import DOMAIN from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_config_flow(hass, mock_get_source_ip): +async def test_config_flow(hass: HomeAssistant, mock_get_source_ip) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -21,7 +24,7 @@ async def test_config_flow(hass, mock_get_source_ip): assert state -async def test_already_setup(hass, mock_get_source_ip): +async def test_already_setup(hass: HomeAssistant, mock_get_source_ip) -> None: """Test we abort if already setup.""" MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index a3fba49b969..21becc39a94 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -1,20 +1,29 @@ """Tests for the local_ip component.""" +from __future__ import annotations + +from homeassistant import config_entries from homeassistant.components.local_ip import DOMAIN from homeassistant.components.network import async_get_source_ip from homeassistant.components.zeroconf import MDNS_TARGET_IP +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_basic_setup(hass, mock_get_source_ip): +async def test_basic_setup(hass: HomeAssistant, mock_get_source_ip) -> None: """Test component setup creates entry from config.""" entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state == config_entries.ConfigEntryState.LOADED local_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) state = hass.states.get(f"sensor.{DOMAIN}") assert state assert state.state == local_ip + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 94b94c2ec93..a0af04145b0 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -18,11 +18,10 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): """Mock device tracker config loading.""" - pass @pytest.fixture -async def locative_client(loop, hass, hass_client): +async def locative_client(event_loop, hass, hass_client): """Locative mock client.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index fb7ac217867..366b4b30ed5 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -489,7 +489,15 @@ async def test_logbook_describe_event(recorder_mock, hass, hass_client): await async_wait_recording_done(hass) client = await hass_client() - response = await client.get("/api/logbook") + # 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}" + ) results = await response.json() assert len(results) == 1 event = results[0] @@ -553,7 +561,15 @@ async def test_exclude_described_event(recorder_mock, hass, hass_client): await async_wait_recording_done(hass) client = await hass_client() - response = await client.get("/api/logbook") + # 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}" + ) results = await response.json() assert len(results) == 1 event = results[0] diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 42f604b3d4c..5b16c98998c 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -913,7 +913,7 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( recorder_mock, hass, hass_ws_client ): - """Test subscribe/unsubscribe logbook stream inherts filters from recorder.""" + """Test subscribe/unsubscribe logbook stream inherits filters from recorder.""" now = dt_util.utcnow() await asyncio.gather( *[ diff --git a/tests/components/logger/conftest.py b/tests/components/logger/conftest.py new file mode 100644 index 00000000000..00d27753a61 --- /dev/null +++ b/tests/components/logger/conftest.py @@ -0,0 +1,12 @@ +"""Test fixtures for the Logger component.""" +import logging + +import pytest + + +@pytest.fixture(autouse=True) +def restore_logging_class(): + """Restore logging class.""" + klass = logging.getLoggerClass() + yield + logging.setLoggerClass(klass) diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 6435ef95394..6b6269c4099 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -3,8 +3,6 @@ from collections import defaultdict import logging from unittest.mock import Mock, patch -import pytest - from homeassistant.components import logger from homeassistant.components.logger import LOGSEVERITY from homeassistant.setup import async_setup_component @@ -15,14 +13,8 @@ ZONE_NS = f"{COMPONENTS_NS}.zone" GROUP_NS = f"{COMPONENTS_NS}.group" CONFIGED_NS = "otherlibx" UNCONFIG_NS = "unconfigurednamespace" - - -@pytest.fixture(autouse=True) -def restore_logging_class(): - """Restore logging class.""" - klass = logging.getLoggerClass() - yield - logging.setLoggerClass(klass) +INTEGRATION = "test_component" +INTEGRATION_NS = f"homeassistant.components.{INTEGRATION}" async def test_log_filtering(hass, caplog): @@ -158,7 +150,7 @@ async def test_setting_level(hass): ) -async def test_can_set_level(hass): +async def test_can_set_level_from_yaml(hass): """Test logger propagation.""" assert await async_setup_component( @@ -178,7 +170,49 @@ async def test_can_set_level(hass): } }, ) + await _assert_log_levels(hass) + _reset_logging() + +async def test_can_set_level_from_store(hass, hass_storage): + """Test setting up logs from store.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + CONFIGED_NS: { + "level": "WARNING", + "persistence": "once", + "type": "module", + }, + f"{CONFIGED_NS}.info": { + "level": "INFO", + "persistence": "once", + "type": "module", + }, + f"{CONFIGED_NS}.debug": { + "level": "DEBUG", + "persistence": "once", + "type": "module", + }, + HASS_NS: {"level": "WARNING", "persistence": "once", "type": "module"}, + COMPONENTS_NS: { + "level": "INFO", + "persistence": "once", + "type": "module", + }, + ZONE_NS: {"level": "DEBUG", "persistence": "once", "type": "module"}, + GROUP_NS: {"level": "INFO", "persistence": "once", "type": "module"}, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component(hass, "logger", {}) + await _assert_log_levels(hass) + _reset_logging() + + +async def _assert_log_levels(hass): assert logging.getLogger(UNCONFIG_NS).level == logging.NOTSET assert logging.getLogger(UNCONFIG_NS).isEnabledFor(logging.CRITICAL) is True assert ( @@ -255,3 +289,113 @@ async def test_can_set_level(hass): assert logging.getLogger(CONFIGED_NS).level == logging.WARNING logging.getLogger("").setLevel(logging.NOTSET) + + +def _reset_logging(): + """Reset loggers.""" + logging.getLogger(CONFIGED_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(f"{CONFIGED_NS}.info").orig_setLevel(logging.NOTSET) + logging.getLogger(f"{CONFIGED_NS}.debug").orig_setLevel(logging.NOTSET) + logging.getLogger(HASS_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(COMPONENTS_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(ZONE_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(GROUP_NS).orig_setLevel(logging.NOTSET) + logging.getLogger(INTEGRATION_NS).orig_setLevel(logging.NOTSET) + + +async def test_can_set_integration_level_from_store(hass, hass_storage): + """Test setting up integration logs from store.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + INTEGRATION: { + "level": "WARNING", + "persistence": "once", + "type": "integration", + }, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component(hass, "logger", {}) + + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.DEBUG) is False + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.WARNING) is True + + _reset_logging() + + +async def test_chattier_log_level_wins_1(hass, hass_storage): + """Test chattier log level in store takes precedence.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + INTEGRATION_NS: { + "level": "DEBUG", + "persistence": "once", + "type": "module", + }, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component( + hass, + "logger", + { + "logger": { + "logs": { + INTEGRATION_NS: "warning", + } + } + }, + ) + + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.DEBUG) is True + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.WARNING) is True + + _reset_logging() + + +async def test_chattier_log_level_wins_2(hass, hass_storage): + """Test chattier log level in yaml takes precedence.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + INTEGRATION_NS: { + "level": "WARNING", + "persistence": "once", + "type": "module", + }, + } + }, + "key": "core.logger", + "version": 1, + } + assert await async_setup_component( + hass, "logger", {"logger": {"logs": {INTEGRATION_NS: "debug"}}} + ) + + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.DEBUG) is True + assert logging.getLogger(INTEGRATION_NS).isEnabledFor(logging.WARNING) is True + + _reset_logging() + + +async def test_log_once_removed_from_store(hass, hass_storage): + """Test logs with persistence "once" are removed from the store at startup.""" + hass_storage["core.logger"] = { + "data": { + "logs": { + ZONE_NS: {"type": "module", "level": "DEBUG", "persistence": "once"} + } + }, + "key": "core.logger", + "version": 1, + } + + assert await async_setup_component(hass, "logger", {}) + + assert hass_storage["core.logger"]["data"] == {"logs": {}} diff --git a/tests/components/logger/test_websocket_api.py b/tests/components/logger/test_websocket_api.py new file mode 100644 index 00000000000..1448196f1a3 --- /dev/null +++ b/tests/components/logger/test_websocket_api.py @@ -0,0 +1,195 @@ +"""Tests for Logger Websocket API commands.""" +import logging + +from homeassistant.components.logger.helpers import async_get_domain_config +from homeassistant.components.websocket_api import const +from homeassistant.setup import async_setup_component + + +async def test_integration_log_info(hass, hass_ws_client, hass_admin_user): + """Test fetching integration log info.""" + + assert await async_setup_component(hass, "logger", {}) + + logging.getLogger("homeassistant.components.http").setLevel(logging.DEBUG) + logging.getLogger("homeassistant.components.websocket_api").setLevel(logging.DEBUG) + + websocket_client = await hass_ws_client() + await websocket_client.send_json({"id": 7, "type": "logger/log_info"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert {"domain": "http", "level": logging.DEBUG} in msg["result"] + assert {"domain": "websocket_api", "level": logging.DEBUG} in msg["result"] + + +async def test_integration_log_level_logger_not_loaded( + hass, hass_ws_client, hass_admin_user +): + """Test setting integration log level.""" + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logger/log_level", + "integration": "websocket_api", + "level": logging.DEBUG, + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + + +async def test_integration_log_level(hass, hass_ws_client, hass_admin_user): + """Test setting integration log level.""" + websocket_client = await hass_ws_client() + assert await async_setup_component(hass, "logger", {}) + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/integration_log_level", + "integration": "websocket_api", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.DEBUG + } + + +async def test_integration_log_level_unknown_integration( + hass, hass_ws_client, hass_admin_user +): + """Test setting integration log level for an unknown integration.""" + websocket_client = await hass_ws_client() + assert await async_setup_component(hass, "logger", {}) + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/integration_log_level", + "integration": "websocket_api_123", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + + +async def test_module_log_level(hass, hass_ws_client, hass_admin_user): + """Test setting integration log level.""" + websocket_client = await hass_ws_client() + assert await async_setup_component( + hass, + "logger", + {"logger": {"logs": {"homeassistant.components.other_component": "warning"}}}, + ) + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.DEBUG, + "homeassistant.components.other_component": logging.WARNING, + } + + +async def test_module_log_level_override(hass, hass_ws_client, hass_admin_user): + """Test override yaml integration log level.""" + websocket_client = await hass_ws_client() + assert await async_setup_component( + hass, + "logger", + {"logger": {"logs": {"homeassistant.components.websocket_api": "warning"}}}, + ) + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.WARNING + } + + await websocket_client.send_json( + { + "id": 6, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "ERROR", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.ERROR + } + + await websocket_client.send_json( + { + "id": 7, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "DEBUG", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.DEBUG + } + + await websocket_client.send_json( + { + "id": 8, + "type": "logger/log_level", + "module": "homeassistant.components.websocket_api", + "level": "NOTSET", + "persistence": "none", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 8 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + assert async_get_domain_config(hass).overrides == { + "homeassistant.components.websocket_api": logging.NOTSET + } diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index a1558822619..e7688f62f06 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -13,6 +13,7 @@ from homeassistant.components.lutron_caseta import ( ATTR_TYPE, ) from homeassistant.components.lutron_caseta.const import ( + ATTR_BUTTON_TYPE, ATTR_LEAP_BUTTON_NUMBER, CONF_CA_CERTS, CONF_CERTFILE, @@ -23,6 +24,7 @@ from homeassistant.components.lutron_caseta.const import ( from homeassistant.components.lutron_caseta.device_trigger import CONF_SUBTYPE from homeassistant.components.lutron_caseta.models import LutronCasetaData from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_DEVICE_ID, CONF_DOMAIN, CONF_HOST, @@ -254,6 +256,8 @@ async def test_if_fires_on_button_event(hass, calls, device_reg): ATTR_DEVICE_NAME: device["Name"], ATTR_AREA_NAME: device.get("Area", {}).get("Name"), ATTR_ACTION: "press", + ATTR_DEVICE_ID: device_id, + ATTR_BUTTON_TYPE: "on", } hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) await hass.async_block_till_done() @@ -298,6 +302,8 @@ async def test_if_fires_on_button_event_without_lip(hass, calls, device_reg): ATTR_DEVICE_NAME: device["Name"], ATTR_AREA_NAME: device.get("Area", {}).get("Name"), ATTR_ACTION: "press", + ATTR_DEVICE_ID: device_id, + ATTR_BUTTON_TYPE: "Kitchen Pendants", } hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) await hass.async_block_till_done() @@ -420,3 +426,56 @@ async def test_validate_trigger_invalid_triggers(hass, device_reg): ] }, ) + + +async def test_if_fires_on_button_event_late_setup(hass, calls): + """Test for press trigger firing with integration getting setup late.""" + config_entry_id = await _async_setup_lutron_with_picos(hass) + await hass.config_entries.async_unload(config_entry_id) + await hass.async_block_till_done() + + device = MOCK_BUTTON_DEVICES[0] + dr = device_registry.async_get(hass) + dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])}) + device_id = dr_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"}, + }, + }, + ] + }, + ) + + await hass.config_entries.async_setup(config_entry_id) + await hass.async_block_till_done() + + message = { + ATTR_SERIAL: device.get("serial"), + ATTR_TYPE: device.get("type"), + ATTR_LEAP_BUTTON_NUMBER: 0, + ATTR_DEVICE_NAME: device["Name"], + ATTR_AREA_NAME: device.get("Area", {}).get("Name"), + ATTR_ACTION: "press", + ATTR_DEVICE_ID: device_id, + ATTR_BUTTON_TYPE: "on", + } + 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" diff --git a/tests/components/matter/__init__.py b/tests/components/matter/__init__.py new file mode 100644 index 00000000000..a2452274b14 --- /dev/null +++ b/tests/components/matter/__init__.py @@ -0,0 +1 @@ +"""Tests for the Matter integration.""" diff --git a/tests/components/matter/common.py b/tests/components/matter/common.py new file mode 100644 index 00000000000..92a4cd4c8f1 --- /dev/null +++ b/tests/components/matter/common.py @@ -0,0 +1,74 @@ +"""Provide common test tools.""" +from __future__ import annotations + +from functools import cache +import json +from typing import Any +from unittest.mock import MagicMock + +from matter_server.common.helpers.util import dataclass_from_dict +from matter_server.common.models.events import EventType +from matter_server.common.models.node import MatterNode + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@cache +def load_node_fixture(fixture: str) -> str: + """Load a fixture.""" + return load_fixture(f"matter/nodes/{fixture}.json") + + +def load_and_parse_node_fixture(fixture: str) -> dict[str, Any]: + """Load and parse a node fixture.""" + return json.loads(load_node_fixture(fixture)) + + +async def setup_integration_with_node_fixture( + hass: HomeAssistant, + node_fixture: str, + client: MagicMock, +) -> MatterNode: + """Set up Matter integration with fixture as node.""" + node_data = load_and_parse_node_fixture(node_fixture) + node = dataclass_from_dict( + MatterNode, + node_data, + ) + client.get_nodes.return_value = [node] + client.get_node.return_value = node + config_entry = MockConfigEntry( + domain="matter", data={"url": "http://mock-matter-server-url"} + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return node + + +def set_node_attribute( + node: MatterNode, + endpoint: int, + cluster_id: int, + attribute_id: int, + value: Any, +) -> None: + """Set a node attribute.""" + attribute = node.attributes[f"{endpoint}/{cluster_id}/{attribute_id}"] + attribute.value = value + + +async def trigger_subscription_callback( + hass: HomeAssistant, + client: MagicMock, + event: EventType = EventType.ATTRIBUTE_UPDATED, + data: Any = None, +) -> None: + """Trigger a subscription callback.""" + callback = client.subscribe.call_args[0][0] + callback(event, data) + await hass.async_block_till_done() diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py new file mode 100644 index 00000000000..03c8bc35687 --- /dev/null +++ b/tests/components/matter/conftest.py @@ -0,0 +1,199 @@ +"""Provide common fixtures.""" +from __future__ import annotations + +import asyncio +from collections.abc import AsyncGenerator, Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from matter_server.common.models.server_information import ServerInfo +import pytest + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_FABRIC_ID = 12341234 +MOCK_COMPR_FABRIC_ID = 1234 + + +@pytest.fixture(name="matter_client") +async def matter_client_fixture() -> AsyncGenerator[MagicMock, None]: + """Fixture for a Matter client.""" + with patch( + "homeassistant.components.matter.MatterClient", autospec=True + ) as client_class: + client = client_class.return_value + + async def connect() -> None: + """Mock connect.""" + await asyncio.sleep(0) + client.connected = True + + async def listen(init_ready: asyncio.Event | None) -> None: + """Mock listen.""" + if init_ready is not None: + init_ready.set() + + client.connect = AsyncMock(side_effect=connect) + client.start_listening = AsyncMock(side_effect=listen) + client.server_info = ServerInfo( + fabric_id=MOCK_FABRIC_ID, + compressed_fabric_id=MOCK_COMPR_FABRIC_ID, + schema_version=1, + sdk_version="2022.11.1", + wifi_credentials_set=True, + thread_credentials_set=True, + ) + + yield client + + +@pytest.fixture(name="integration") +async def integration_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MockConfigEntry: + """Set up the Matter integration.""" + entry = MockConfigEntry(domain="matter", data={"url": "ws://localhost:5580/ws"}) + 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="create_backup") +def create_backup_fixture() -> Generator[AsyncMock, None, None]: + """Mock Supervisor create backup of add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_create_backup" + ) as create_backup: + yield create_backup + + +@pytest.fixture(name="addon_store_info") +def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock Supervisor add-on store info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + +@pytest.fixture(name="addon_info") +def addon_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_info", + ) as addon_info: + addon_info.return_value = { + "hostname": None, + "options": {}, + "state": None, + "update_available": False, + "version": None, + } + yield addon_info + + +@pytest.fixture(name="addon_not_installed") +def addon_not_installed_fixture( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> AsyncMock: + """Mock add-on not installed.""" + return addon_info + + +@pytest.fixture(name="addon_installed") +def addon_installed_fixture( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> AsyncMock: + """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-matter-server" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_running") +def addon_running_fixture( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> AsyncMock: + """Mock add-on already running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } + addon_info.return_value["hostname"] = "core-matter-server" + addon_info.return_value["state"] = "started" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="install_addon") +def install_addon_fixture( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> Generator[AsyncMock, None, None]: + """Mock install add-on.""" + + async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None: + """Mock install add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + + with patch( + "homeassistant.components.hassio.addon_manager.async_install_addon" + ) as install_addon: + install_addon.side_effect = install_addon_side_effect + yield install_addon + + +@pytest.fixture(name="start_addon") +def start_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock start add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_start_addon" + ) as start_addon: + yield start_addon + + +@pytest.fixture(name="stop_addon") +def stop_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock stop add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_stop_addon" + ) as stop_addon: + yield stop_addon + + +@pytest.fixture(name="uninstall_addon") +def uninstall_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon + + +@pytest.fixture(name="update_addon") +def update_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock update add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_update_addon" + ) as update_addon: + yield update_addon diff --git a/tests/components/matter/fixtures/nodes/contact-sensor.json b/tests/components/matter/fixtures/nodes/contact-sensor.json new file mode 100644 index 00000000000..2aec6a32516 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/contact-sensor.json @@ -0,0 +1,656 @@ +{ + "node_id": 1, + "date_commissioned": "2022-11-29T21:23:48.485051", + "last_interview": "2022-11-29T21:23:48.485057", + "interview_version": 1, + "attributes": { + "0/29/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, + 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "Mock ContactSensor" + }, + "0/40/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "Mock Contact sensor" + }, + "0/40/6": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_name": "Location", + "value": "XX" + }, + "0/40/7": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20221206" + }, + "0/40/12": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "mock-contact-sensor" + }, + "0/40/19": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65531, 65532, 65533 + ] + }, + + "1/3/0": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyTime", + "attribute_name": "IdentifyTime", + "value": 0 + }, + "1/3/1": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyType", + "attribute_name": "IdentifyType", + "value": 2 + }, + "1/3/65532": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/3/65533": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/3/65528": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/3/65529": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 64] + }, + "1/3/65531": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + + "1/29/0": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 21, + "revision": 1 + } + ] + }, + "1/29/1": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 3, 4, 5, 6, 7, 8, 15, 29, 30, 37, 47, 59, 64, 65, 69, 80, 257, 258, 259, + 512, 513, 514, 516, 768, 1024, 1026, 1027, 1028, 1029, 1030, 1283, 1284, + 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 2820, + 4294048773 + ] + }, + "1/29/2": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [] + }, + "1/29/3": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [] + }, + "1/29/65532": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/29/65533": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/29/65528": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/29/65529": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/29/65531": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "1/69/0": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 69, + "cluster_type": "chip.clusters.Objects.BooleanState", + "cluster_name": "BooleanState", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.BooleanState.Attributes.StateValue", + "attribute_name": "StateValue", + "value": true + }, + "1/69/65532": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 69, + "cluster_type": "chip.clusters.Objects.BooleanState", + "cluster_name": "BooleanState", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.BooleanState.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/69/65533": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 69, + "cluster_type": "chip.clusters.Objects.BooleanState", + "cluster_name": "BooleanState", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.BooleanState.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/69/65528": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 69, + "cluster_type": "chip.clusters.Objects.BooleanState", + "cluster_name": "BooleanState", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.BooleanState.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/69/65529": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 69, + "cluster_type": "chip.clusters.Objects.BooleanState", + "cluster_name": "BooleanState", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.BooleanState.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/69/65531": { + "node_id": 1, + "endpoint": 1, + "cluster_id": 69, + "cluster_type": "chip.clusters.Objects.BooleanState", + "cluster_name": "BooleanState", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.BooleanState.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + } + }, + "endpoints": [0, 1], + "root_device_type_instance": null, + "aggregator_device_type_instance": null, + "device_type_instances": [null], + "node_devices": [null], + "_type": "matter_server.common.models.node.MatterNode" +} diff --git a/tests/components/matter/fixtures/nodes/dimmable-light.json b/tests/components/matter/fixtures/nodes/dimmable-light.json new file mode 100644 index 00000000000..03067468f24 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/dimmable-light.json @@ -0,0 +1,4220 @@ +{ + "node_id": 1, + "date_commissioned": "2022-11-29T21:23:48.485051", + "last_interview": "2022-11-29T21:23:48.485057", + "interview_version": 1, + "attributes": { + "0/4/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "0/4/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/4/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "0/4/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "0/4/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "0/4/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/29/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, + 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/31/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Acl", + "attribute_name": "Acl", + "value": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ] + }, + "0/31/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Extension", + "attribute_name": "Extension", + "value": [] + }, + "0/31/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.SubjectsPerAccessControlEntry", + "attribute_name": "SubjectsPerAccessControlEntry", + "value": 4 + }, + "0/31/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.TargetsPerAccessControlEntry", + "attribute_name": "TargetsPerAccessControlEntry", + "value": 3 + }, + "0/31/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AccessControlEntriesPerFabric", + "attribute_name": "AccessControlEntriesPerFabric", + "value": 3 + }, + "0/31/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/31/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/31/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/31/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/31/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "Mock Dimmable Light" + }, + "0/40/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "Mock Dimmable Light" + }, + "0/40/6": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_name": "Location", + "value": "XX" + }, + "0/40/7": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20200101" + }, + "0/40/12": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "mock-dimmable-light" + }, + "0/40/19": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.Basic", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/42/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOtaProviders", + "attribute_name": "DefaultOtaProviders", + "value": [] + }, + "0/42/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible", + "attribute_name": "UpdatePossible", + "value": true + }, + "0/42/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateState", + "attribute_name": "UpdateState", + "value": 0 + }, + "0/42/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress", + "attribute_name": "UpdateStateProgress", + "value": 0 + }, + "0/42/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/42/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/42/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/42/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/42/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/43/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ActiveLocale", + "attribute_name": "ActiveLocale", + "value": "en-US" + }, + "0/43/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.SupportedLocales", + "attribute_name": "SupportedLocales", + "value": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ] + }, + "0/43/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/43/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/43/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/43/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/43/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "0/44/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.HourFormat", + "attribute_name": "HourFormat", + "value": 0 + }, + "0/44/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ActiveCalendarType", + "attribute_name": "ActiveCalendarType", + "value": 0 + }, + "0/44/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.SupportedCalendarTypes", + "attribute_name": "SupportedCalendarTypes", + "value": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7] + }, + "0/44/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/44/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/44/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/44/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/44/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/48/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.Breadcrumb", + "attribute_name": "Breadcrumb", + "value": 0 + }, + "0/48/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.BasicCommissioningInfo", + "attribute_name": "BasicCommissioningInfo", + "value": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + } + }, + "0/48/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.RegulatoryConfig", + "attribute_name": "RegulatoryConfig", + "value": 0 + }, + "0/48/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.LocationCapability", + "attribute_name": "LocationCapability", + "value": 0 + }, + "0/48/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.SupportsConcurrentConnection", + "attribute_name": "SupportsConcurrentConnection", + "value": true + }, + "0/48/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/48/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/48/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5] + }, + "0/48/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4] + }, + "0/48/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/49/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.MaxNetworks", + "attribute_name": "MaxNetworks", + "value": 1 + }, + "0/49/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.Networks", + "attribute_name": "Networks", + "value": [ + { + "networkID": "b'wifi-sm'", + "connected": true + } + ] + }, + "0/49/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ScanMaxTimeSeconds", + "attribute_name": "ScanMaxTimeSeconds", + "value": 10 + }, + "0/49/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ConnectMaxTimeSeconds", + "attribute_name": "ConnectMaxTimeSeconds", + "value": 30 + }, + "0/49/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.InterfaceEnabled", + "attribute_name": "InterfaceEnabled", + "value": true + }, + "0/49/5": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkingStatus", + "attribute_name": "LastNetworkingStatus", + "value": 0 + }, + "0/49/6": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkID", + "attribute_name": "LastNetworkID", + "value": "b'wifi-sm'" + }, + "0/49/7": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastConnectErrorValue", + "attribute_name": "LastConnectErrorValue", + "value": null + }, + "0/49/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/49/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/49/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 5, 7] + }, + "0/49/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 8] + }, + "0/49/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533] + }, + "0/50/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/50/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/50/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1] + }, + "0/50/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/50/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/51/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.NetworkInterfaces", + "attribute_name": "NetworkInterfaces", + "value": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "b\"\\x84\\xf7\\x03'\\xb5,\"", + "IPv4Addresses": ["b'\\xc0\\xa8\\x01\\x84'"], + "IPv6Addresses": [ + "b\"\\xfe\\x80\\x00\\x00\\x00\\x00\\x00\\x00\\x86\\xf7\\x03\\xff\\xfe'\\xb5,\"", + "b\"*\\x00\\xbb\\xa0\\x12\\x12\\x8f\\x00\\x86\\xf7\\x03\\xff\\xfe'\\xb5,\"", + "b\"\\xfd\\xf5\\nn\\xf0cO\\xed\\x86\\xf7\\x03\\xff\\xfe'\\xb5,\"" + ], + "type": 1 + } + ] + }, + "0/51/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.RebootCount", + "attribute_name": "RebootCount", + "value": 6 + }, + "0/51/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.UpTime", + "attribute_name": "UpTime", + "value": 31279 + }, + "0/51/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TotalOperationalHours", + "attribute_name": "TotalOperationalHours", + "value": 8 + }, + "0/51/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.BootReasons", + "attribute_name": "BootReasons", + "value": 1 + }, + "0/51/5": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveHardwareFaults", + "attribute_name": "ActiveHardwareFaults", + "value": [] + }, + "0/51/6": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveRadioFaults", + "attribute_name": "ActiveRadioFaults", + "value": [] + }, + "0/51/7": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveNetworkFaults", + "attribute_name": "ActiveNetworkFaults", + "value": [] + }, + "0/51/8": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TestEventTriggersEnabled", + "attribute_name": "TestEventTriggersEnabled", + "value": false + }, + "0/51/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/51/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/51/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/51/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/51/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533] + }, + "0/52/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ThreadMetrics", + "attribute_name": "ThreadMetrics", + "value": [] + }, + "0/52/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapFree", + "attribute_name": "CurrentHeapFree", + "value": 166480 + }, + "0/52/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapUsed", + "attribute_name": "CurrentHeapUsed", + "value": 86512 + }, + "0/52/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapHighWatermark", + "attribute_name": "CurrentHeapHighWatermark", + "value": 157052 + }, + "0/52/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/52/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/52/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/52/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/52/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/53/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Channel", + "attribute_name": "Channel", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RoutingRole", + "attribute_name": "RoutingRole", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NetworkName", + "attribute_name": "NetworkName", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PanId", + "attribute_name": "PanId", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ExtendedPanId", + "attribute_name": "ExtendedPanId", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/5": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.MeshLocalPrefix", + "attribute_name": "MeshLocalPrefix", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/6": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/7": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NeighborTableList", + "attribute_name": "NeighborTableList", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/8": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouteTableList", + "attribute_name": "RouteTableList", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/9": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionId", + "attribute_name": "PartitionId", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/10": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Weighting", + "attribute_name": "Weighting", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/11": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DataVersion", + "attribute_name": "DataVersion", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/12": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.StableDataVersion", + "attribute_name": "StableDataVersion", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/13": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRouterId", + "attribute_name": "LeaderRouterId", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/14": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DetachedRoleCount", + "attribute_name": "DetachedRoleCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/15": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChildRoleCount", + "attribute_name": "ChildRoleCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/16": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouterRoleCount", + "attribute_name": "RouterRoleCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/17": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRoleCount", + "attribute_name": "LeaderRoleCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/18": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttachAttemptCount", + "attribute_name": "AttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/19": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionIdChangeCount", + "attribute_name": "PartitionIdChangeCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/20": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.BetterPartitionAttachAttemptCount", + "attribute_name": "BetterPartitionAttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/21": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 21, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ParentChangeCount", + "attribute_name": "ParentChangeCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/22": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 22, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxTotalCount", + "attribute_name": "TxTotalCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/23": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 23, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxUnicastCount", + "attribute_name": "TxUnicastCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/24": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 24, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBroadcastCount", + "attribute_name": "TxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/25": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 25, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckRequestedCount", + "attribute_name": "TxAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/26": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 26, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckedCount", + "attribute_name": "TxAckedCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/27": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 27, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxNoAckRequestedCount", + "attribute_name": "TxNoAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/28": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 28, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataCount", + "attribute_name": "TxDataCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/29": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 29, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataPollCount", + "attribute_name": "TxDataPollCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/30": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 30, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconCount", + "attribute_name": "TxBeaconCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/31": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 31, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconRequestCount", + "attribute_name": "TxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/32": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 32, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxOtherCount", + "attribute_name": "TxOtherCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/33": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 33, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxRetryCount", + "attribute_name": "TxRetryCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/34": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 34, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDirectMaxRetryExpiryCount", + "attribute_name": "TxDirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/35": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 35, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxIndirectMaxRetryExpiryCount", + "attribute_name": "TxIndirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/36": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 36, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrCcaCount", + "attribute_name": "TxErrCcaCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/37": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 37, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrAbortCount", + "attribute_name": "TxErrAbortCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/38": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 38, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrBusyChannelCount", + "attribute_name": "TxErrBusyChannelCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/39": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 39, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxTotalCount", + "attribute_name": "RxTotalCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/40": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 40, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxUnicastCount", + "attribute_name": "RxUnicastCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/41": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 41, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBroadcastCount", + "attribute_name": "RxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/42": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 42, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataCount", + "attribute_name": "RxDataCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/43": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 43, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataPollCount", + "attribute_name": "RxDataPollCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/44": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 44, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconCount", + "attribute_name": "RxBeaconCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/45": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 45, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconRequestCount", + "attribute_name": "RxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/46": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 46, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxOtherCount", + "attribute_name": "RxOtherCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/47": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 47, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxAddressFilteredCount", + "attribute_name": "RxAddressFilteredCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/48": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 48, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDestAddrFilteredCount", + "attribute_name": "RxDestAddrFilteredCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/49": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 49, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDuplicatedCount", + "attribute_name": "RxDuplicatedCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/50": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 50, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrNoFrameCount", + "attribute_name": "RxErrNoFrameCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/51": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 51, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrUnknownNeighborCount", + "attribute_name": "RxErrUnknownNeighborCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/52": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 52, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrInvalidSrcAddrCount", + "attribute_name": "RxErrInvalidSrcAddrCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/53": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 53, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrSecCount", + "attribute_name": "RxErrSecCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/54": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 54, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrFcsCount", + "attribute_name": "RxErrFcsCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/55": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 55, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrOtherCount", + "attribute_name": "RxErrOtherCount", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/56": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 56, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveTimestamp", + "attribute_name": "ActiveTimestamp", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/57": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 57, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PendingTimestamp", + "attribute_name": "PendingTimestamp", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/58": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 58, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Delay", + "attribute_name": "Delay", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/59": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 59, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.SecurityPolicy", + "attribute_name": "SecurityPolicy", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/60": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 60, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChannelPage0Mask", + "attribute_name": "ChannelPage0Mask", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/61": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 61, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OperationalDatasetComponents", + "attribute_name": "OperationalDatasetComponents", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/62": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 62, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveNetworkFaultsList", + "attribute_name": "ActiveNetworkFaultsList", + "value": { + "TLVValue": null, + "Reason": "InteractionModelError: Failure (0x1)" + } + }, + "0/53/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 15 + }, + "0/53/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/53/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/53/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/53/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/54/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Bssid", + "attribute_name": "Bssid", + "value": "b'r\\xa7A\\x91\\xf1!'" + }, + "0/54/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.SecurityType", + "attribute_name": "SecurityType", + "value": 4 + }, + "0/54/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.WiFiVersion", + "attribute_name": "WiFiVersion", + "value": 3 + }, + "0/54/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ChannelNumber", + "attribute_name": "ChannelNumber", + "value": 6 + }, + "0/54/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Rssi", + "attribute_name": "Rssi", + "value": -61 + }, + "0/54/5": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconLostCount", + "attribute_name": "BeaconLostCount", + "value": null + }, + "0/54/6": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconRxCount", + "attribute_name": "BeaconRxCount", + "value": null + }, + "0/54/7": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastRxCount", + "attribute_name": "PacketMulticastRxCount", + "value": null + }, + "0/54/8": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastTxCount", + "attribute_name": "PacketMulticastTxCount", + "value": null + }, + "0/54/9": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastRxCount", + "attribute_name": "PacketUnicastRxCount", + "value": null + }, + "0/54/10": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastTxCount", + "attribute_name": "PacketUnicastTxCount", + "value": null + }, + "0/54/11": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate", + "attribute_name": "CurrentMaxRate", + "value": null + }, + "0/54/12": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": null + }, + "0/54/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/54/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/54/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/54/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/54/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, 65532, + 65533 + ] + }, + "0/55/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PHYRate", + "attribute_name": "PHYRate", + "value": null + }, + "0/55/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FullDuplex", + "attribute_name": "FullDuplex", + "value": null + }, + "0/55/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketRxCount", + "attribute_name": "PacketRxCount", + "value": 0 + }, + "0/55/3": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketTxCount", + "attribute_name": "PacketTxCount", + "value": 0 + }, + "0/55/4": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TxErrCount", + "attribute_name": "TxErrCount", + "value": 0 + }, + "0/55/5": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CollisionCount", + "attribute_name": "CollisionCount", + "value": 0 + }, + "0/55/6": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": 0 + }, + "0/55/7": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CarrierDetect", + "attribute_name": "CarrierDetect", + "value": null + }, + "0/55/8": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TimeSinceReset", + "attribute_name": "TimeSinceReset", + "value": 0 + }, + "0/55/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/55/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/55/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/55/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/55/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533] + }, + "0/59/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/59/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/59/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/59/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/59/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/60/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.WindowStatus", + "attribute_name": "WindowStatus", + "value": 0 + }, + "0/60/1": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminFabricIndex", + "attribute_name": "AdminFabricIndex", + "value": null + }, + "0/60/2": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminVendorId", + "attribute_name": "AdminVendorId", + "value": null + }, + "0/60/65532": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/60/65533": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/60/65528": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/60/65529": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2] + }, + "0/60/65531": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/62/0": { + "node_id": 1, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.NOCs", + "attribute_name": "NOCs", + "value": [ + { + "noc": "b'\\x150\\x01\\x01\\x01$\\x02\\x017\\x03$\\x13\\x02\\x18&\\x04\\x80\"\\x81\\'&\\x05\\x80%M:7\\x06$\\x15\\x01$\\x11\\x01\\x18$\\x07\\x01$\\x08\\x010\\tA\\x04Z\\xec\\xd9\\x11h\\'J[%&\\xe1\\xd8\\xa0\\xb15\\x01\\x9c\\x83\\xf8\\x1a\\x9bg\\xbb\\x81e\\x18\\x8b\\xc8\\x9dr\\xed\\xe7\\xc3\\\\\\xd21\\x99\\xc1\\xdb\\xfe\\xd0}\\xf5)\\xaeMq\\xd5\\xeeRg\\x16\\xd5%\\x9c\\xfcD\\t\\xcbQ\\xc8\\x83*\\x997\\n5\\x01(\\x01\\x18$\\x02\\x016\\x03\\x04\\x02\\x04\\x01\\x180\\x04\\x14\\xca\\xe2\\x07\\xdb\\x89 None: + """Test bridge devices are set up correctly with via_device.""" + await setup_integration_with_node_fixture( + hass, hass_storage, "lighting-example-app" + ) + + dev_reg = dr.async_get(hass) + + entry = dev_reg.async_get_device({(DOMAIN, "BE8F70AA40DDAE41")}) + assert entry is not None + + assert entry.name == "My Cool Light" + assert entry.manufacturer == "Nabu Casa" + assert entry.model == "M5STAMP Lighting App" + assert entry.hw_version == "v1.0" + assert entry.sw_version == "55ab764bea" + + +async def test_device_registry_bridge( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test bridge devices are set up correctly with via_device.""" + await setup_integration_with_node_fixture( + hass, hass_storage, "fake-bridge-two-light" + ) + + dev_reg = dr.async_get(hass) + + # Validate bridge + bridge_entry = dev_reg.async_get_device({(DOMAIN, "mock-hub-id")}) + assert bridge_entry is not None + + assert bridge_entry.name == "My Mock Bridge" + assert bridge_entry.manufacturer == "Mock Vendor" + assert bridge_entry.model == "Mock Bridge" + assert bridge_entry.hw_version == "TEST_VERSION" + assert bridge_entry.sw_version == "123.4.5" + + # Device 1 + device1_entry = dev_reg.async_get_device({(DOMAIN, "mock-id-kitchen-ceiling")}) + assert device1_entry is not None + + assert device1_entry.via_device_id == bridge_entry.id + assert device1_entry.name == "Kitchen Ceiling" + assert device1_entry.manufacturer == "Mock Vendor" + assert device1_entry.model == "Mock Light" + assert device1_entry.hw_version is None + assert device1_entry.sw_version == "67.8.9" + + # Device 2 + device2_entry = dev_reg.async_get_device({(DOMAIN, "mock-id-living-room-ceiling")}) + assert device2_entry is not None + + assert device2_entry.via_device_id == bridge_entry.id + assert device2_entry.name == "Living Room Ceiling" + assert device2_entry.manufacturer == "Mock Vendor" + assert device2_entry.model == "Mock Light" + assert device2_entry.hw_version is None + assert device2_entry.sw_version == "1.49.1" diff --git a/tests/components/matter/test_api.py b/tests/components/matter/test_api.py new file mode 100644 index 00000000000..6fe18d7c3b1 --- /dev/null +++ b/tests/components/matter/test_api.py @@ -0,0 +1,179 @@ +"""Test the api module.""" +from collections.abc import Awaitable, Callable +from unittest.mock import MagicMock, call + +from aiohttp import ClientWebSocketResponse +from matter_server.client.exceptions import FailedCommand + +from homeassistant.components.matter.api import ID, TYPE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_commission( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + matter_client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test the commission command.""" + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "matter/commission", + "code": "12345678", + } + ) + msg = await ws_client.receive_json() + + assert msg["success"] + matter_client.commission_with_code.assert_called_once_with("12345678") + + matter_client.commission_with_code.reset_mock() + matter_client.commission_with_code.side_effect = FailedCommand( + "test_id", "test_code", "Failed to commission" + ) + + await ws_client.send_json( + { + ID: 2, + TYPE: "matter/commission", + "code": "12345678", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "test_code" + matter_client.commission_with_code.assert_called_once_with("12345678") + + +async def test_commission_on_network( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + matter_client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test the commission on network command.""" + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "matter/commission_on_network", + "pin": 1234, + } + ) + msg = await ws_client.receive_json() + + assert msg["success"] + matter_client.commission_on_network.assert_called_once_with(1234) + + matter_client.commission_on_network.reset_mock() + matter_client.commission_on_network.side_effect = FailedCommand( + "test_id", "test_code", "Failed to commission on network" + ) + + await ws_client.send_json( + { + ID: 2, + TYPE: "matter/commission_on_network", + "pin": 1234, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "test_code" + matter_client.commission_on_network.assert_called_once_with(1234) + + +async def test_set_thread_dataset( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + matter_client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test the set thread dataset command.""" + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "matter/set_thread", + "thread_operation_dataset": "test_dataset", + } + ) + msg = await ws_client.receive_json() + + assert msg["success"] + matter_client.set_thread_operational_dataset.assert_called_once_with("test_dataset") + + matter_client.set_thread_operational_dataset.reset_mock() + matter_client.set_thread_operational_dataset.side_effect = FailedCommand( + "test_id", "test_code", "Failed to commission" + ) + + await ws_client.send_json( + { + ID: 2, + TYPE: "matter/set_thread", + "thread_operation_dataset": "test_dataset", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "test_code" + matter_client.set_thread_operational_dataset.assert_called_once_with("test_dataset") + + +async def test_set_wifi_credentials( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + matter_client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test the set WiFi credentials command.""" + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 1, + TYPE: "matter/set_wifi_credentials", + "network_name": "test_network", + "password": "test_password", + } + ) + msg = await ws_client.receive_json() + + assert msg["success"] + assert matter_client.set_wifi_credentials.call_count == 1 + assert matter_client.set_wifi_credentials.call_args == call( + ssid="test_network", credentials="test_password" + ) + + matter_client.set_wifi_credentials.reset_mock() + matter_client.set_wifi_credentials.side_effect = FailedCommand( + "test_id", "test_code", "Failed to commission on network" + ) + + await ws_client.send_json( + { + ID: 2, + TYPE: "matter/set_wifi_credentials", + "network_name": "test_network", + "password": "test_password", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "test_code" + assert matter_client.set_wifi_credentials.call_count == 1 + assert matter_client.set_wifi_credentials.call_args == call( + ssid="test_network", credentials="test_password" + ) diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py new file mode 100644 index 00000000000..522cda2dccc --- /dev/null +++ b/tests/components/matter/test_binary_sensor.py @@ -0,0 +1,69 @@ +"""Test Matter binary sensors.""" +from unittest.mock import MagicMock + +from matter_server.common.models.node import MatterNode +import pytest + +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="contact_sensor_node") +async def contact_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a contact sensor node.""" + return await setup_integration_with_node_fixture( + hass, "contact-sensor", matter_client + ) + + +async def test_contact_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + contact_sensor_node: MatterNode, +) -> None: + """Test contact sensor.""" + state = hass.states.get("binary_sensor.mock_contact_sensor_contact") + assert state + assert state.state == "on" + + set_node_attribute(contact_sensor_node, 1, 69, 0, False) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.mock_contact_sensor_contact") + assert state + assert state.state == "off" + + +@pytest.fixture(name="occupancy_sensor_node") +async def occupancy_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a occupancy sensor node.""" + return await setup_integration_with_node_fixture( + hass, "occupancy-sensor", matter_client + ) + + +async def test_occupancy_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + occupancy_sensor_node: MatterNode, +) -> None: + """Test occupancy sensor.""" + state = hass.states.get("binary_sensor.mock_occupancy_sensor_occupancy") + assert state + assert state.state == "on" + + set_node_attribute(occupancy_sensor_node, 1, 1030, 0, 0) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.mock_occupancy_sensor_occupancy") + assert state + assert state.state == "off" diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py new file mode 100644 index 00000000000..cad8cd91eb0 --- /dev/null +++ b/tests/components/matter/test_config_flow.py @@ -0,0 +1,979 @@ +"""Test the Matter config flow.""" +from __future__ import annotations + +from collections.abc import Generator +from typing import Any +from unittest.mock import DEFAULT, AsyncMock, MagicMock, call, patch + +from matter_server.client.exceptions import CannotConnect, InvalidServerVersion +import pytest + +from homeassistant import config_entries +from homeassistant.components.hassio import HassioAPIError, HassioServiceInfo +from homeassistant.components.matter.const import ADDON_SLUG, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +ADDON_DISCOVERY_INFO = { + "addon": "Matter Server", + "host": "host1", + "port": 5581, +} + + +@pytest.fixture(name="setup_entry", autouse=True) +def setup_entry_fixture() -> Generator[AsyncMock, None, None]: + """Mock entry setup.""" + with patch( + "homeassistant.components.matter.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="client_connect", autouse=True) +def client_connect_fixture() -> Generator[AsyncMock, None, None]: + """Mock server version.""" + with patch( + "homeassistant.components.matter.config_flow.MatterClient.connect" + ) as client_connect: + yield client_connect + + +@pytest.fixture(name="supervisor") +def supervisor_fixture() -> Generator[MagicMock, None, None]: + """Mock Supervisor.""" + with patch( + "homeassistant.components.matter.config_flow.is_hassio", return_value=True + ) as is_hassio: + yield is_hassio + + +@pytest.fixture(name="discovery_info") +def discovery_info_fixture() -> Any: + """Return the discovery info from the supervisor.""" + return DEFAULT + + +@pytest.fixture(name="get_addon_discovery_info", autouse=True) +def get_addon_discovery_info_fixture( + discovery_info: Any, +) -> Generator[AsyncMock, None, None]: + """Mock get add-on discovery info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info", + return_value=discovery_info, + ) as get_addon_discovery_info: + yield get_addon_discovery_info + + +@pytest.fixture(name="addon_setup_time", autouse=True) +def addon_setup_time_fixture() -> Generator[int, None, None]: + """Mock add-on setup sleep time.""" + with patch( + "homeassistant.components.matter.config_flow.ADDON_SETUP_TIMEOUT", new=0 + ) as addon_setup_time: + yield addon_setup_time + + +async def test_manual_create_entry( + hass: HomeAssistant, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test user step create entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://localhost:5580/ws" + assert result["data"] == { + "url": "ws://localhost:5580/ws", + "integration_created_addon": False, + "use_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize( + "error, side_effect", + [ + ("cannot_connect", CannotConnect(Exception("Boom"))), + ("invalid_server_version", InvalidServerVersion("Invalid version")), + ("unknown", Exception("Unknown boom")), + ], +) +async def test_manual_errors( + hass: HomeAssistant, + client_connect: AsyncMock, + error: str, + side_effect: Exception, +) -> None: + """Test user step cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + client_connect.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": error} + + +async def test_manual_already_configured( + hass: HomeAssistant, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test manual step abort if already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, data={"url": "ws://host1:5581/ws"}, title="Matter" + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfiguration_successful" + assert entry.data["url"] == "ws://localhost:5580/ws" + assert entry.data["use_addon"] is False + assert entry.data["integration_created_addon"] is False + assert entry.title == "ws://localhost:5580/ws" + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_supervisor_discovery( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test flow started from Supervisor discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Matter Server", + slug=ADDON_SLUG, + ), + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert client_connect.call_count == 0 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://host1:5581/ws" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, error", + [({"config": ADDON_DISCOVERY_INFO}, HassioAPIError())], +) +async def test_supervisor_discovery_addon_info_failed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + error: Exception, +) -> None: + """Test Supervisor discovery and addon info failed.""" + addon_info.side_effect = error + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Matter Server", + slug=ADDON_SLUG, + ), + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert addon_info.call_count == 1 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_info_failed" + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_clean_supervisor_discovery_on_user_create( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test discovery flow is cleaned up when a user flow is finished.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Matter Server", + slug=ADDON_SLUG, + ), + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": False} + ) + + assert addon_info.call_count == 0 + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "manual" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress()) == 0 + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://localhost:5580/ws" + assert result["data"] == { + "url": "ws://localhost:5580/ws", + "use_addon": False, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +async def test_abort_supervisor_discovery_with_existing_entry( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, +) -> None: + """Test discovery flow is aborted if an entry already exists.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"url": "ws://localhost:5580/ws"}, + title="ws://localhost:5580/ws", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Matter Server", + slug=ADDON_SLUG, + ), + ) + + assert addon_info.call_count == 0 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_supervisor_discovery_with_existing_flow( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, +) -> None: + """Test hassio discovery flow is aborted when another flow is in progress.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Matter Server", + slug=ADDON_SLUG, + ), + ) + + assert addon_info.call_count == 0 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_abort_supervisor_discovery_for_other_addon( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, +) -> None: + """Test hassio discovery flow is aborted for a non official add-on discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config={ + "addon": "Other Matter Server", + "host": "host1", + "port": 3001, + }, + name="Other Matter Server", + slug="other_addon", + ), + ) + + assert addon_info.call_count == 0 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_matter_addon" + + +async def test_supervisor_discovery_addon_not_running( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test discovery with add-on already installed but not running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Matter Server", + slug=ADDON_SLUG, + ), + ) + + assert addon_info.call_count == 0 + assert result["step_id"] == "hassio_confirm" + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert addon_info.call_count == 1 + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://host1:5581/ws" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +async def test_supervisor_discovery_addon_not_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_not_installed: AsyncMock, + install_addon: AsyncMock, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test discovery with add-on not installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Matter Server", + slug=ADDON_SLUG, + ), + ) + + assert addon_info.call_count == 0 + assert addon_store_info.call_count == 0 + assert result["step_id"] == "hassio_confirm" + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert addon_info.call_count == 0 + assert addon_store_info.call_count == 1 + assert result["step_id"] == "install_addon" + assert result["type"] == FlowResultType.SHOW_PROGRESS + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert install_addon.call_args == call(hass, "core_matter_server") + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://host1:5581/ws" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": True, + } + assert setup_entry.call_count == 1 + + +async def test_not_addon( + hass: HomeAssistant, + supervisor: MagicMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test opting out of add-on on Supervisor.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": False} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "manual" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5581/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://localhost:5581/ws" + assert result["data"] == { + "url": "ws://localhost:5581/ws", + "use_addon": False, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_running( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test add-on already running on Supervisor.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://host1:5581/ws" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, discovery_info_error, client_connect_error, addon_info_error, " + "abort_reason, discovery_info_called, client_connect_called", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + None, + "cannot_connect", + True, + True, + ), + ( + None, + None, + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + None, + HassioAPIError(), + "addon_info_failed", + False, + False, + ), + ], +) +async def test_addon_running_failures( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + discovery_info_error: Exception | None, + client_connect_error: Exception | None, + addon_info_error: Exception | None, + abort_reason: str, + discovery_info_called: bool, + client_connect_called: bool, +) -> None: + """Test all failures when add-on is running.""" + get_addon_discovery_info.side_effect = discovery_info_error + client_connect.side_effect = client_connect_error + addon_info.side_effect = addon_info_error + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert addon_info.call_count == 1 + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == abort_reason + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_running_already_configured( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test that only one instance is allowed when add-on is running.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "url": "ws://localhost:5580/ws", + }, + title="ws://localhost:5580/ws", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfiguration_successful" + assert entry.data["url"] == "ws://host1:5581/ws" + assert entry.title == "ws://host1:5581/ws" + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, + start_addon: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test add-on already installed but not running on Supervisor.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert addon_info.call_count == 1 + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call(hass, "core_matter_server") + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://host1:5581/ws" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, start_addon_error, client_connect_error, " + "discovery_info_called, client_connect_called", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + False, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + True, + True, + ), + ( + None, + None, + None, + True, + False, + ), + ], +) +async def test_addon_installed_failures( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, + start_addon: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + start_addon_error: Exception | None, + client_connect_error: Exception | None, + discovery_info_called: bool, + client_connect_called: bool, +) -> None: + """Test add-on start failure when add-on is installed.""" + start_addon.side_effect = start_addon_error + client_connect.side_effect = client_connect_error + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert addon_info.call_count == 1 + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert start_addon.call_args == call(hass, "core_matter_server") + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_installed_already_configured( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, + start_addon: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test that only one instance is allowed when add-on is installed.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "url": "ws://localhost:5580/ws", + }, + title="ws://localhost:5580/ws", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert addon_info.call_count == 1 + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call(hass, "core_matter_server") + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfiguration_successful" + assert entry.data["url"] == "ws://host1:5581/ws" + assert entry.title == "ws://host1:5581/ws" + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_not_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_not_installed: AsyncMock, + install_addon: AsyncMock, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + start_addon: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test add-on not installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert addon_info.call_count == 0 + assert addon_store_info.call_count == 1 + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + + # 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 install_addon.call_args == call(hass, "core_matter_server") + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call(hass, "core_matter_server") + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "ws://host1:5581/ws" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": True, + } + assert setup_entry.call_count == 1 + + +async def test_addon_not_installed_failures( + hass: HomeAssistant, + supervisor: MagicMock, + addon_not_installed: AsyncMock, + addon_info: AsyncMock, + install_addon: AsyncMock, +) -> None: + """Test add-on install failure.""" + install_addon.side_effect = HassioAPIError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + + # 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 install_addon.call_args == call(hass, "core_matter_server") + assert addon_info.call_count == 0 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_not_installed_already_configured( + hass: HomeAssistant, + supervisor: MagicMock, + addon_not_installed: AsyncMock, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test that only one instance is allowed when add-on is not installed.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "url": "ws://localhost:5580/ws", + }, + title="ws://localhost:5580/ws", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert addon_info.call_count == 0 + assert addon_store_info.call_count == 1 + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + + # 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 install_addon.call_args == call(hass, "core_matter_server") + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfiguration_successful" + assert entry.data["url"] == "ws://host1:5581/ws" + assert entry.title == "ws://host1:5581/ws" + assert setup_entry.call_count == 1 diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py new file mode 100644 index 00000000000..f34e428ecc0 --- /dev/null +++ b/tests/components/matter/test_init.py @@ -0,0 +1,398 @@ +"""Test the Matter integration init.""" +from __future__ import annotations + +import asyncio +from unittest.mock import AsyncMock, MagicMock, call + +from matter_server.client.exceptions import InvalidServerVersion +import pytest + +from homeassistant.components.hassio import HassioAPIError +from homeassistant.components.matter.const import DOMAIN +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_raise_addon_task_in_progress( + hass: HomeAssistant, + addon_not_installed: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test raise ConfigEntryNotReady if an add-on task is in progress.""" + install_event = asyncio.Event() + + install_addon_original_side_effect = install_addon.side_effect + + async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None: + """Mock install add-on.""" + await install_event.wait() + await install_addon_original_side_effect(hass, slug) + + install_addon.side_effect = install_addon_side_effect + + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={ + "url": "ws://host1:5581/ws", + "use_addon": True, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await asyncio.sleep(0.05) + + assert entry.state is ConfigEntryState.SETUP_RETRY + assert install_addon.call_count == 1 + assert start_addon.call_count == 0 + + # Check that we only call install add-on once if a task is in progress. + await hass.config_entries.async_reload(entry.entry_id) + await asyncio.sleep(0.05) + + assert entry.state is ConfigEntryState.SETUP_RETRY + assert install_addon.call_count == 1 + assert start_addon.call_count == 0 + + install_event.set() + await hass.async_block_till_done() + + assert install_addon.call_count == 1 + assert start_addon.call_count == 1 + + +async def test_start_addon( + hass: HomeAssistant, + addon_installed: AsyncMock, + addon_info: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test start the Matter Server add-on during entry setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={ + "url": "ws://host1:5581/ws", + "use_addon": True, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY + assert addon_info.call_count == 1 + assert install_addon.call_count == 0 + assert start_addon.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + + +async def test_install_addon( + hass: HomeAssistant, + addon_not_installed: AsyncMock, + addon_store_info: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test install and start the Matter add-on during entry setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={ + "url": "ws://host1:5581/ws", + "use_addon": True, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY + assert addon_store_info.call_count == 2 + assert install_addon.call_count == 1 + assert install_addon.call_args == call(hass, "core_matter_server") + assert start_addon.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + + +async def test_addon_info_failure( + hass: HomeAssistant, + addon_installed: AsyncMock, + addon_info: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test failure to get add-on info for Matter add-on during entry setup.""" + addon_info.side_effect = HassioAPIError("Boom") + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={ + "url": "ws://host1:5581/ws", + "use_addon": True, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY + assert addon_info.call_count == 1 + assert install_addon.call_count == 0 + assert start_addon.call_count == 0 + + +@pytest.mark.parametrize( + "addon_version, update_available, update_calls, backup_calls, " + "update_addon_side_effect, create_backup_side_effect", + [ + ("1.0.0", True, 1, 1, None, None), + ("1.0.0", False, 0, 0, None, None), + ("1.0.0", True, 1, 1, HassioAPIError("Boom"), None), + ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")), + ], +) +async def test_update_addon( + hass: HomeAssistant, + addon_installed: AsyncMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, + matter_client: MagicMock, + addon_version: str, + update_available: bool, + update_calls: int, + backup_calls: int, + update_addon_side_effect: Exception | None, + create_backup_side_effect: Exception | None, +): + """Test update the Matter add-on during entry setup.""" + addon_info.return_value["version"] = addon_version + addon_info.return_value["update_available"] = update_available + create_backup.side_effect = create_backup_side_effect + update_addon.side_effect = update_addon_side_effect + matter_client.connect.side_effect = InvalidServerVersion("Invalid version") + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={ + "url": "ws://host1:5581/ws", + "use_addon": True, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY + assert create_backup.call_count == backup_calls + assert update_addon.call_count == update_calls + + +async def test_issue_registry_invalid_version( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test issue registry for invalid version.""" + original_connect_side_effect = matter_client.connect.side_effect + matter_client.connect.side_effect = InvalidServerVersion("Invalid version") + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={ + "url": "ws://host1:5581/ws", + "use_addon": False, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + issue_reg = ir.async_get(hass) + entry_state = entry.state + assert entry_state is ConfigEntryState.SETUP_RETRY + assert issue_reg.async_get_issue(DOMAIN, "invalid_server_version") + + matter_client.connect.side_effect = original_connect_side_effect + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert not issue_reg.async_get_issue(DOMAIN, "invalid_server_version") + + +@pytest.mark.parametrize( + "stop_addon_side_effect, entry_state", + [ + (None, ConfigEntryState.NOT_LOADED), + (HassioAPIError("Boom"), ConfigEntryState.LOADED), + ], +) +async def test_stop_addon( + hass, + matter_client: MagicMock, + addon_installed: AsyncMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + stop_addon: AsyncMock, + stop_addon_side_effect: Exception | None, + entry_state: ConfigEntryState, +): + """Test stop the Matter add-on on entry unload if entry is disabled.""" + stop_addon.side_effect = stop_addon_side_effect + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={ + "url": "ws://host1:5581/ws", + "use_addon": True, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert addon_info.call_count == 1 + addon_info.reset_mock() + + await hass.config_entries.async_set_disabled_by( + entry.entry_id, ConfigEntryDisabler.USER + ) + await hass.async_block_till_done() + + assert entry.state == entry_state + assert stop_addon.call_count == 1 + assert stop_addon.call_args == call(hass, "core_matter_server") + + +async def test_remove_entry( + hass: HomeAssistant, + addon_installed: AsyncMock, + stop_addon: AsyncMock, + create_backup: AsyncMock, + uninstall_addon: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test remove the config entry.""" + # test successful remove without created add-on + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + data={"integration_created_addon": False}, + ) + entry.add_to_hass(hass) + assert entry.state is ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + await hass.config_entries.async_remove(entry.entry_id) + + assert entry.state is ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + # test successful remove with created add-on + entry = MockConfigEntry( + domain=DOMAIN, + title="Matter", + 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 stop_addon.call_args == call(hass, "core_matter_server") + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, + {"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]}, + partial=True, + ) + assert uninstall_addon.call_count == 1 + assert uninstall_addon.call_args == call(hass, "core_matter_server") + assert entry.state is ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + stop_addon.reset_mock() + create_backup.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 stop_addon.call_args == call(hass, "core_matter_server") + assert create_backup.call_count == 0 + assert uninstall_addon.call_count == 0 + assert entry.state is ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to stop the Matter Server add-on" in caplog.text + stop_addon.side_effect = None + stop_addon.reset_mock() + create_backup.reset_mock() + uninstall_addon.reset_mock() + + # test create backup failure + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + create_backup.side_effect = HassioAPIError() + + await hass.config_entries.async_remove(entry.entry_id) + + assert stop_addon.call_count == 1 + assert stop_addon.call_args == call(hass, "core_matter_server") + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, + {"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]}, + partial=True, + ) + assert uninstall_addon.call_count == 0 + assert entry.state is ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to create a backup of the Matter Server add-on" in caplog.text + create_backup.side_effect = None + stop_addon.reset_mock() + create_backup.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 stop_addon.call_args == call(hass, "core_matter_server") + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, + {"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]}, + partial=True, + ) + assert uninstall_addon.call_count == 1 + assert uninstall_addon.call_args == call(hass, "core_matter_server") + assert entry.state is ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to uninstall the Matter Server add-on" in caplog.text diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py new file mode 100644 index 00000000000..883eedaefb7 --- /dev/null +++ b/tests/components/matter/test_light.py @@ -0,0 +1,106 @@ +"""Test Matter lights.""" +from unittest.mock import MagicMock, call + +from chip.clusters import Objects as clusters +from matter_server.common.models.node import MatterNode +import pytest + +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="light_node") +async def light_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a light node.""" + return await setup_integration_with_node_fixture( + hass, "dimmable-light", matter_client + ) + + +async def test_turn_on( + hass: HomeAssistant, + matter_client: MagicMock, + light_node: MatterNode, +) -> None: + """Test turning on a light.""" + state = hass.states.get("light.mock_dimmable_light") + assert state + assert state.state == "on" + + set_node_attribute(light_node, 1, 6, 0, False) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("light.mock_dimmable_light") + assert state + assert state.state == "off" + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.mock_dimmable_light", + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=light_node.node_id, + endpoint=1, + command=clusters.OnOff.Commands.On(), + ) + matter_client.send_device_command.reset_mock() + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.mock_dimmable_light", + "brightness": 128, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=light_node.node_id, + endpoint=1, + command=clusters.LevelControl.Commands.MoveToLevelWithOnOff( + level=128, + transitionTime=0, + ), + ) + + +async def test_turn_off( + hass: HomeAssistant, + matter_client: MagicMock, + light_node: MatterNode, +) -> None: + """Test turning off a light.""" + state = hass.states.get("light.mock_dimmable_light") + assert state + assert state.state == "on" + + await hass.services.async_call( + "light", + "turn_off", + { + "entity_id": "light.mock_dimmable_light", + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=light_node.node_id, + endpoint=1, + command=clusters.OnOff.Commands.Off(), + ) diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py new file mode 100644 index 00000000000..ccdf09b8dc9 --- /dev/null +++ b/tests/components/matter/test_sensor.py @@ -0,0 +1,169 @@ +"""Test Matter sensors.""" +from unittest.mock import MagicMock + +from matter_server.common.models.node import MatterNode +import pytest + +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="flow_sensor_node") +async def flow_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a flow sensor node.""" + return await setup_integration_with_node_fixture(hass, "flow-sensor", matter_client) + + +@pytest.fixture(name="humidity_sensor_node") +async def humidity_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a humidity sensor node.""" + return await setup_integration_with_node_fixture( + hass, "humidity-sensor", matter_client + ) + + +@pytest.fixture(name="light_sensor_node") +async def light_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a light sensor node.""" + return await setup_integration_with_node_fixture( + hass, "light-sensor", matter_client + ) + + +@pytest.fixture(name="pressure_sensor_node") +async def pressure_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a pressure sensor node.""" + return await setup_integration_with_node_fixture( + hass, "pressure-sensor", matter_client + ) + + +@pytest.fixture(name="temperature_sensor_node") +async def temperature_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a temperature sensor node.""" + return await setup_integration_with_node_fixture( + hass, "temperature-sensor", matter_client + ) + + +async def test_sensor_null_value( + hass: HomeAssistant, + matter_client: MagicMock, + flow_sensor_node: MatterNode, +) -> None: + """Test flow sensor.""" + state = hass.states.get("sensor.mock_flow_sensor_flow") + assert state + assert state.state == "0.0" + + set_node_attribute(flow_sensor_node, 1, 1028, 0, None) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_flow_sensor_flow") + assert state + assert state.state == "unknown" + + +async def test_flow_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + flow_sensor_node: MatterNode, +) -> None: + """Test flow sensor.""" + state = hass.states.get("sensor.mock_flow_sensor_flow") + assert state + assert state.state == "0.0" + + set_node_attribute(flow_sensor_node, 1, 1028, 0, 20) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_flow_sensor_flow") + assert state + assert state.state == "2.0" + + +async def test_humidity_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + humidity_sensor_node: MatterNode, +) -> None: + """Test humidity sensor.""" + state = hass.states.get("sensor.mock_humidity_sensor_humidity") + assert state + assert state.state == "0.0" + + set_node_attribute(humidity_sensor_node, 1, 1029, 0, 4000) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_humidity_sensor_humidity") + assert state + assert state.state == "40.0" + + +async def test_light_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + light_sensor_node: MatterNode, +) -> None: + """Test light sensor.""" + state = hass.states.get("sensor.mock_light_sensor_light") + assert state + assert state.state == "1.3" + + set_node_attribute(light_sensor_node, 1, 1024, 0, 3000) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_light_sensor_light") + assert state + assert state.state == "2.0" + + +async def test_pressure_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + pressure_sensor_node: MatterNode, +) -> None: + """Test pressure sensor.""" + state = hass.states.get("sensor.mock_pressure_sensor_pressure") + assert state + assert state.state == "0.0" + + set_node_attribute(pressure_sensor_node, 1, 1027, 0, 1010) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_pressure_sensor_pressure") + assert state + assert state.state == "101.0" + + +async def test_temperature_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + temperature_sensor_node: MatterNode, +) -> None: + """Test temperature sensor.""" + state = hass.states.get("sensor.mock_temperature_sensor_temperature") + assert state + assert state.state == "21.0" + + set_node_attribute(temperature_sensor_node, 1, 1026, 0, 2500) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_temperature_sensor_temperature") + assert state + assert state.state == "25.0" diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py new file mode 100644 index 00000000000..a79edd6010b --- /dev/null +++ b/tests/components/matter/test_switch.py @@ -0,0 +1,85 @@ +"""Test Matter switches.""" +from unittest.mock import MagicMock, call + +from chip.clusters import Objects as clusters +from matter_server.common.models.node import MatterNode +import pytest + +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="switch_node") +async def switch_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a switch node.""" + return await setup_integration_with_node_fixture( + hass, "on-off-plugin-unit", matter_client + ) + + +async def test_turn_on( + hass: HomeAssistant, + matter_client: MagicMock, + switch_node: MatterNode, +) -> None: + """Test turning on a switch.""" + state = hass.states.get("switch.mock_onoff_plugin_unit") + assert state + assert state.state == "off" + + await hass.services.async_call( + "switch", + "turn_on", + { + "entity_id": "switch.mock_onoff_plugin_unit", + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=switch_node.node_id, + endpoint=1, + command=clusters.OnOff.Commands.On(), + ) + + set_node_attribute(switch_node, 1, 6, 0, True) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("switch.mock_onoff_plugin_unit") + assert state + assert state.state == "on" + + +async def test_turn_off( + hass: HomeAssistant, + matter_client: MagicMock, + switch_node: MatterNode, +) -> None: + """Test turning off a switch.""" + state = hass.states.get("switch.mock_onoff_plugin_unit") + assert state + assert state.state == "off" + + await hass.services.async_call( + "switch", + "turn_off", + { + "entity_id": "switch.mock_onoff_plugin_unit", + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=switch_node.node_id, + endpoint=1, + command=clusters.OnOff.Commands.Off(), + ) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 22206133fca..6c2ebda023a 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -136,7 +136,7 @@ async def test_media_browse(hass, hass_ws_client): client = await hass_ws_client(hass) with patch( - "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", + "homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features", MediaPlayerEntityFeature.BROWSE_MEDIA, ), patch( "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", @@ -179,7 +179,7 @@ async def test_media_browse(hass, hass_ws_client): assert mock_browse_media.mock_calls[0][1] == ("album", "abcd") with patch( - "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", + "homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features", MediaPlayerEntityFeature.BROWSE_MEDIA, ), patch( "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", @@ -210,7 +210,7 @@ async def test_group_members_available_when_off(hass): # Fake group support for DemoYoutubePlayer with patch( - "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", + "homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features", MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.TURN_OFF, ): await hass.services.async_call( diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index 47265444105..e95241cbbc7 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -15,8 +15,9 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def meraki_client(loop, hass, hass_client): +def meraki_client(event_loop, hass, hass_client): """Meraki mock client.""" + loop = event_loop assert loop.run_until_complete( async_setup_component( hass, diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index 0eb334763d6..2503eefb1b0 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize("platform", ("sensor",)) -async def test_config_flow(hass: HomeAssistant, platform) -> None: +async def test_config_flow(hass: HomeAssistant, platform: str) -> None: """Test the config flow.""" input_sensors = ["sensor.input_one", "sensor.input_two"] @@ -66,7 +66,7 @@ def get_suggested(schema, key): @pytest.mark.parametrize("platform", ("sensor",)) -async def test_options(hass: HomeAssistant, platform) -> None: +async def test_options(hass: HomeAssistant, platform: str) -> None: """Test reconfiguring.""" hass.states.async_set("sensor.input_one", "10") hass.states.async_set("sensor.input_two", "20") diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 4819cc31a9b..9ba043427b5 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -2,6 +2,8 @@ import statistics from unittest.mock import patch +from pytest import LogCaptureFixture + from homeassistant import config as hass_config from homeassistant.components.min_max.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass @@ -14,12 +16,14 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er from homeassistant.setup import async_setup_component from tests.common import get_fixture_path VALUES = [17, 20, 15.3] +VALUES_ERROR = [17, "string", 15.3] COUNT = len(VALUES) MIN_VALUE = min(VALUES) MAX_VALUE = max(VALUES) @@ -29,9 +33,10 @@ MEAN_4_DIGITS = round(sum(VALUES) / COUNT, 4) MEDIAN = round(statistics.median(VALUES), 2) RANGE_1_DIGIT = round(max(VALUES) - min(VALUES), 1) RANGE_4_DIGITS = round(max(VALUES) - min(VALUES), 4) +SUM_VALUE = sum(VALUES) -async def test_default_name_sensor(hass): +async def test_default_name_sensor(hass: HomeAssistant) -> None: """Test the min sensor with a default name.""" config = { "sensor": { @@ -56,7 +61,7 @@ async def test_default_name_sensor(hass): assert entity_ids[2] == state.attributes.get("min_entity_id") -async def test_min_sensor(hass): +async def test_min_sensor(hass: HomeAssistant) -> None: """Test the min sensor.""" config = { "sensor": { @@ -88,7 +93,7 @@ async def test_min_sensor(hass): assert entity.unique_id == "very_unique_id" -async def test_max_sensor(hass): +async def test_max_sensor(hass: HomeAssistant) -> None: """Test the max sensor.""" config = { "sensor": { @@ -115,7 +120,7 @@ async def test_max_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_mean_sensor(hass): +async def test_mean_sensor(hass: HomeAssistant) -> None: """Test the mean sensor.""" config = { "sensor": { @@ -141,7 +146,7 @@ async def test_mean_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_mean_1_digit_sensor(hass): +async def test_mean_1_digit_sensor(hass: HomeAssistant) -> None: """Test the mean with 1-digit precision sensor.""" config = { "sensor": { @@ -167,7 +172,7 @@ async def test_mean_1_digit_sensor(hass): assert str(float(MEAN_1_DIGIT)) == state.state -async def test_mean_4_digit_sensor(hass): +async def test_mean_4_digit_sensor(hass: HomeAssistant) -> None: """Test the mean with 4-digit precision sensor.""" config = { "sensor": { @@ -193,7 +198,7 @@ async def test_mean_4_digit_sensor(hass): assert str(float(MEAN_4_DIGITS)) == state.state -async def test_median_sensor(hass): +async def test_median_sensor(hass: HomeAssistant) -> None: """Test the median sensor.""" config = { "sensor": { @@ -219,7 +224,7 @@ async def test_median_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_range_4_digit_sensor(hass): +async def test_range_4_digit_sensor(hass: HomeAssistant) -> None: """Test the range with 4-digit precision sensor.""" config = { "sensor": { @@ -245,7 +250,7 @@ async def test_range_4_digit_sensor(hass): assert str(float(RANGE_4_DIGITS)) == state.state -async def test_range_1_digit_sensor(hass): +async def test_range_1_digit_sensor(hass: HomeAssistant) -> None: """Test the range with 1-digit precision sensor.""" config = { "sensor": { @@ -271,7 +276,7 @@ async def test_range_1_digit_sensor(hass): assert str(float(RANGE_1_DIGIT)) == state.state -async def test_not_enough_sensor_value(hass): +async def test_not_enough_sensor_value(hass: HomeAssistant) -> None: """Test that there is nothing done if not enough values available.""" config = { "sensor": { @@ -323,7 +328,7 @@ async def test_not_enough_sensor_value(hass): assert state.attributes.get("max_value") is None -async def test_different_unit_of_measurement(hass): +async def test_different_unit_of_measurement(hass: HomeAssistant) -> None: """Test for different unit of measurement.""" config = { "sensor": { @@ -370,7 +375,7 @@ async def test_different_unit_of_measurement(hass): assert state.attributes.get("unit_of_measurement") == "ERR" -async def test_last_sensor(hass): +async def test_last_sensor(hass: HomeAssistant) -> None: """Test the last sensor.""" config = { "sensor": { @@ -395,7 +400,7 @@ async def test_last_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload filter sensors.""" hass.states.async_set("sensor.test_1", 12345) hass.states.async_set("sensor.test_2", 45678) @@ -433,3 +438,89 @@ async def test_reload(hass): assert hass.states.get("sensor.test") is None assert hass.states.get("sensor.second_test") + + +async def test_sensor_incorrect_state( + hass: HomeAssistant, caplog: LogCaptureFixture +) -> None: + """Test the min sensor.""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_failure", + "type": "min", + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_failure") + + assert state.state == "15.3" + assert "Unable to store state. Only numerical states are supported" in caplog.text + + +async def test_sum_sensor(hass: HomeAssistant) -> None: + """Test the sum sensor.""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_sum", + "type": "sum", + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_sum_sensor", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + + assert str(float(SUM_VALUE)) == state.state + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entity_reg = er.async_get(hass) + entity = entity_reg.async_get("sensor.test_sum") + assert entity.unique_id == "very_unique_id_sum_sensor" + + +async def test_sum_sensor_no_state(hass: HomeAssistant) -> None: + """Test the sum sensor with no state .""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_sum", + "type": "sum", + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_sum_sensor", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + + assert state.state == STATE_UNKNOWN diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 30036c0aba7..0794aab0fda 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -6,7 +6,8 @@ from unittest.mock import patch import pytest from homeassistant.components.camera import CameraEntityFeature -from homeassistant.components.mobile_app.const import CONF_SECRET +from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN +from homeassistant.components.tag import EVENT_TAG_SCANNED from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( CONF_WEBHOOK_ID, @@ -16,11 +17,11 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE -from tests.common import async_mock_service +from tests.common import async_capture_events, async_mock_service def encrypt_payload(secret_key, payload, encode_json=True): @@ -840,14 +841,10 @@ async def test_webhook_camera_stream_stream_available_but_errors( async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_client): """Test that we can scan tags.""" - events = [] + device = dr.async_get(hass).async_get_device({(DOMAIN, "mock-device-id")}) + assert device is not None - @callback - def store_event(event): - """Help store events.""" - events.append(event) - - hass.bus.async_listen("tag_scanned", store_event) + events = async_capture_events(hass, EVENT_TAG_SCANNED) resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), @@ -860,7 +857,7 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien assert len(events) == 1 assert events[0].data["tag_id"] == "mock-tag-id" - assert events[0].data["device_id"] == "mock-device-id" + assert events[0].data["device_id"] == device.id async def test_register_sensor_limits_state_class( diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 777d284e20f..611f558cf1f 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, CONF_INPUT_TYPE, CONF_LAZY_ERROR, CONF_SLAVE_COUNT, @@ -54,6 +55,16 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") } ] }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SLAVE: 10, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + } + ] + }, ], ) async def test_config_binary_sensor(hass, mock_modbus): @@ -91,41 +102,65 @@ async def test_config_binary_sensor(hass, mock_modbus): }, ], }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + }, + ], + }, ], ) @pytest.mark.parametrize( "register_words,do_exception,expected", [ ( - [0xFF], + [True] * 8, False, STATE_ON, ), ( - [0x01], + [False] * 8, + False, + STATE_OFF, + ), + ( + [False] + [True] * 7, + False, + STATE_OFF, + ), + ( + [True] + [False] * 7, False, STATE_ON, ), ( - [0x00], - False, - STATE_OFF, - ), - ( - [0x80], - False, - STATE_OFF, - ), - ( - [0xFE], - False, - STATE_OFF, - ), - ( - [0x00], + [False] * 8, True, STATE_UNAVAILABLE, ), + ( + [1] * 8, + False, + STATE_ON, + ), + ( + [2] * 8, + False, + STATE_OFF, + ), + ( + [4] + [1] * 7, + False, + STATE_OFF, + ), + ( + [1] + [8] * 7, + False, + STATE_ON, + ), ], ) async def test_all_binary_sensor(hass, expected, mock_do_cycle): @@ -153,7 +188,7 @@ async def test_all_binary_sensor(hass, expected, mock_do_cycle): "register_words,do_exception,start_expect,end_expect", [ ( - [0x00], + [False * 16], True, STATE_UNKNOWN, STATE_UNAVAILABLE, @@ -269,6 +304,34 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + ] + }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + } + ] + }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + ] + }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, } ] }, @@ -279,106 +342,33 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): [ ( {CONF_SLAVE_COUNT: 1}, - [0x01], - STATE_ON, - [ - STATE_OFF, - ], + [False] * 8, + STATE_OFF, + [STATE_OFF], ), ( {CONF_SLAVE_COUNT: 1}, - [0x02], - STATE_OFF, - [ - STATE_ON, - ], + [True] + [False] * 7, + STATE_ON, + [STATE_OFF], ), ( {CONF_SLAVE_COUNT: 1}, - [0x04], + [False, True] + [False] * 6, STATE_OFF, - [ - STATE_OFF, - ], + [STATE_ON], ), ( {CONF_SLAVE_COUNT: 7}, - [0x01], + [True, False] * 4, STATE_ON, - [ - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - ], + [STATE_OFF, STATE_ON] * 3 + [STATE_OFF], ), ( - {CONF_SLAVE_COUNT: 7}, - [0x82], - STATE_OFF, - [ - STATE_ON, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_ON, - ], - ), - ( - {CONF_SLAVE_COUNT: 10}, - [0x01, 0x00], + {CONF_SLAVE_COUNT: 31}, + [True, False] * 16, STATE_ON, - [ - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - ], - ), - ( - {CONF_SLAVE_COUNT: 10}, - [0x01, 0x01], - STATE_ON, - [ - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_ON, - STATE_OFF, - STATE_OFF, - ], - ), - ( - {CONF_SLAVE_COUNT: 10}, - [0x81, 0x01], - STATE_ON, - [ - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_ON, - STATE_ON, - STATE_OFF, - STATE_OFF, - ], + [STATE_OFF, STATE_ON] * 15 + [STATE_OFF], ), ], ) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index e554160d5bb..f9e43ae077b 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -10,6 +10,13 @@ from homeassistant.components.climate.const import ( from homeassistant.components.modbus.const import ( CONF_CLIMATES, CONF_DATA_TYPE, + CONF_HVAC_MODE_AUTO, + CONF_HVAC_MODE_COOL, + CONF_HVAC_MODE_DRY, + CONF_HVAC_MODE_FAN_ONLY, + CONF_HVAC_MODE_HEAT, + CONF_HVAC_MODE_HEAT_COOL, + CONF_HVAC_MODE_OFF, CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, CONF_HVAC_ONOFF_REGISTER, @@ -82,13 +89,13 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 11, CONF_HVAC_MODE_VALUES: { - HVACMode.OFF.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.COOL.value: 2, - HVACMode.HEAT_COOL.value: 3, - HVACMode.DRY.value: 4, - HVACMode.FAN_ONLY.value: 5, - HVACMode.AUTO.value: 6, + "state_off": 0, + "state_heat": 1, + "state_cool": 2, + "state_heat_cool": 3, + "state_dry": 4, + "state_fan_only": 5, + "state_auto": 6, }, }, } @@ -114,10 +121,12 @@ async def test_config_climate(hass, mock_modbus): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 11, CONF_HVAC_MODE_VALUES: { - HVACMode.OFF.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.COOL.value: 2, - HVACMode.HEAT_COOL.value: 3, + CONF_HVAC_MODE_OFF: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_COOL: 2, + CONF_HVAC_MODE_HEAT_COOL: 3, + CONF_HVAC_MODE_AUTO: 4, + CONF_HVAC_MODE_FAN_ONLY: 5, }, }, } @@ -132,6 +141,8 @@ async def test_config_hvac_mode_register(hass, mock_modbus): assert HVACMode.HEAT in state.attributes[ATTR_HVAC_MODES] assert HVACMode.COOL in state.attributes[ATTR_HVAC_MODES] assert HVACMode.HEAT_COOL in state.attributes[ATTR_HVAC_MODES] + assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES] + assert HVACMode.FAN_ONLY in state.attributes[ATTR_HVAC_MODES] @pytest.mark.parametrize( @@ -203,9 +214,9 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.DRY.value: 2, + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_DRY: 2, }, }, }, @@ -227,9 +238,9 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.DRY.value: 2, + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_DRY: 2, }, }, }, @@ -251,9 +262,9 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 0, - HVACMode.HEAT.value: 2, - HVACMode.DRY.value: 3, + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 2, + CONF_HVAC_MODE_DRY: 3, }, }, CONF_HVAC_ONOFF_REGISTER: 119, @@ -374,8 +385,8 @@ async def test_service_climate_set_temperature( CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 1, - HVACMode.HEAT.value: 2, + CONF_HVAC_MODE_COOL: 1, + CONF_HVAC_MODE_HEAT: 2, }, }, } @@ -395,8 +406,8 @@ async def test_service_climate_set_temperature( CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 1, - HVACMode.HEAT.value: 2, + CONF_HVAC_MODE_COOL: 1, + CONF_HVAC_MODE_HEAT: 2, }, }, CONF_HVAC_ONOFF_REGISTER: 119, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 11ddf4bc426..3b704eff161 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -16,6 +16,7 @@ from datetime import timedelta import logging from unittest import mock +from freezegun.api import FrozenDateTimeFactory from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest import pytest @@ -529,7 +530,7 @@ async def test_pb_service_write( data[do_write[DATA]], ) if do_return[DATA]: - assert caplog.messages[-1].startswith("Pymodbus:") + assert any(message.startswith("Pymodbus:") for message in caplog.messages) @pytest.fixture(name="mock_modbus_read_pymodbus") @@ -542,6 +543,7 @@ async def mock_modbus_read_pymodbus_fixture( do_exception, caplog, mock_pymodbus, + freezer: FrozenDateTimeFactory, ): """Load integration modbus using mocked pymodbus.""" caplog.clear() @@ -573,16 +575,13 @@ async def mock_modbus_read_pymodbus_fixture( } ], } - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() assert DOMAIN in hass.config.components assert caplog.text == "" - now = now + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() yield mock_pymodbus @@ -690,7 +689,7 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): assert ExceptionMessage in caplog.text -async def test_delay(hass, mock_pymodbus): +async def test_delay(hass, mock_pymodbus, freezer: FrozenDateTimeFactory): """Run test for startup delay.""" # the purpose of this test is to test startup delay @@ -720,11 +719,8 @@ async def test_delay(hass, mock_pymodbus): } mock_pymodbus.read_coils.return_value = ReadResult([0x01]) start_time = dt_util.utcnow() - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", return_value=start_time - ): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNKNOWN time_sensor_active = start_time + timedelta(seconds=2) @@ -733,19 +729,18 @@ async def test_delay(hass, mock_pymodbus): time_stop = time_after_scan + timedelta(seconds=10) now = start_time while now < time_stop: - now += timedelta(seconds=1) - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", - return_value=now, - autospec=True, - ): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - if now > time_sensor_active: - if now <= time_after_delay: - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - elif now > time_after_scan: - assert hass.states.get(entity_id).state == STATE_ON + # This test assumed listeners are always fired at 0 + # microseconds which is impossible in production so + # we use 999999 microseconds to simulate the real world. + freezer.tick(timedelta(seconds=1, microseconds=999999)) + now = dt_util.utcnow() + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + if now > time_sensor_active: + if now <= time_after_delay: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + elif now > time_after_scan: + assert hass.states.get(entity_id).state == STATE_ON @pytest.mark.parametrize( @@ -850,19 +845,20 @@ async def test_write_no_client(hass, mock_modbus): @pytest.mark.parametrize("do_config", [{}]) -async def test_integration_reload(hass, caplog, mock_modbus): +async def test_integration_reload( + hass, caplog, mock_modbus, freezer: FrozenDateTimeFactory +): """Run test for integration reload.""" caplog.set_level(logging.INFO) caplog.clear() yaml_path = get_fixture_path("configuration.yaml", "modbus") - now = dt_util.utcnow() with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() for i in range(4): - now = now + timedelta(seconds=1) - async_fire_time_changed(hass, now) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert "Modbus reloading" in caplog.text diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index b1432876a97..bb9e4285c42 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -717,6 +717,7 @@ async def test_lazy_error_sensor(hass, mock_do_cycle, start_expect, end_expect): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 1, }, ], }, @@ -762,6 +763,84 @@ async def test_struct_sensor(hass, mock_do_cycle, expected): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 201, + CONF_SCAN_INTERVAL: 1, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "config_addon,register_words,expected", + [ + ( + { + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_NONE, + CONF_DATA_TYPE: DataType.UINT16, + }, + [0x0102], + "258", + ), + ( + { + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_BYTE, + CONF_DATA_TYPE: DataType.UINT16, + }, + [0x0102], + "513", + ), + ( + { + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_NONE, + CONF_DATA_TYPE: DataType.UINT32, + }, + [0x0102, 0x0304], + "16909060", + ), + ( + { + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_BYTE, + CONF_DATA_TYPE: DataType.UINT32, + }, + [0x0102, 0x0304], + "33620995", + ), + ( + { + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD, + CONF_DATA_TYPE: DataType.UINT32, + }, + [0x0102, 0x0304], + "50594050", + ), + ( + { + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD_BYTE, + CONF_DATA_TYPE: DataType.UINT32, + }, + [0x0102, 0x0304], + "67305985", + ), + ], +) +async def test_wrap_sensor(hass, mock_do_cycle, expected): + """Run test for sensor struct.""" + assert hass.states.get(ENTITY_ID).state == expected + + @pytest.mark.parametrize( "mock_test_state", [(State(ENTITY_ID, "117"), State(f"{ENTITY_ID}_1", "119"))], diff --git a/tests/components/modem_callerid/test_init.py b/tests/components/modem_callerid/test_init.py index 0465fb24a07..de49e229ef1 100644 --- a/tests/components/modem_callerid/test_init.py +++ b/tests/components/modem_callerid/test_init.py @@ -1,5 +1,5 @@ """Test Modem Caller ID integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from phone_modem import exceptions @@ -20,7 +20,7 @@ async def test_setup_entry(hass: HomeAssistant): data={CONF_DEVICE: com_port().device}, ) entry.add_to_hass(hass) - with patch("aioserial.AioSerial", return_value=AsyncMock()), patch( + with patch("aioserial.AioSerial", autospec=True), patch( "homeassistant.components.modem_callerid.PhoneModem._get_response", return_value="OK", ), patch("phone_modem.PhoneModem._modem_sm"): diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 3e2db3bf897..d70405cd297 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -62,7 +62,7 @@ from tests.common import async_fire_time_changed @pytest.fixture -def aiohttp_server(loop, aiohttp_server, socket_enabled): +def aiohttp_server(event_loop, aiohttp_server, socket_enabled): """Return aiohttp_server and allow opening sockets.""" return aiohttp_server @@ -218,7 +218,7 @@ async def test_get_still_image_from_camera( ) -> None: """Test getting a still image.""" - image_handler = Mock(return_value="") + image_handler = AsyncMock(return_value="") app = web.Application() app.add_routes( @@ -258,7 +258,7 @@ async def test_get_still_image_from_camera( async def test_get_stream_from_camera(aiohttp_server: Any, hass: HomeAssistant) -> None: """Test getting a stream.""" - stream_handler = Mock(return_value="") + stream_handler = AsyncMock(return_value="") app = web.Application() app.add_routes([web.get("/", stream_handler)]) stream_server = await aiohttp_server(app) @@ -341,7 +341,7 @@ async def test_camera_option_stream_url_template( """Verify camera with a stream URL template option.""" client = create_mock_motioneye_client() - stream_handler = Mock(return_value="") + stream_handler = AsyncMock(return_value="") app = web.Application() app.add_routes([web.get(f"/{TEST_CAMERA_NAME}/{TEST_CAMERA_ID}", stream_handler)]) stream_server = await aiohttp_server(app) @@ -371,7 +371,7 @@ async def test_camera_option_stream_url_template( # the expected exception, then verify the right handler was called. with pytest.raises(HTTPBadGateway): await async_get_mjpeg_stream(hass, Mock(), TEST_CAMERA_ENTITY_ID) - assert stream_handler.called + assert AsyncMock.called assert not client.get_camera_stream_url.called diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index d305d2ae7aa..5d9ed872beb 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -54,7 +54,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -117,11 +116,6 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def alarm_control_panel_platform_only(): @@ -991,15 +985,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = alarm_control_panel.DOMAIN @@ -1014,16 +999,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = alarm_control_panel.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 48acde5c6c9..91607de9343 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -39,7 +39,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_reload_with_config, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setup_manual_entity_from_yaml, @@ -64,11 +63,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def binary_sensor_platform_only(): @@ -117,14 +111,15 @@ async def test_setting_sensor_value_expires( """Test the expiration of the value.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "expire_after": 4, - "force_update": True, + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + } } }, ) @@ -1016,15 +1011,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "payload1, state1, payload2, state2", [("ON", "on", "OFF", "off"), ("OFF", "off", "ON", "on")], @@ -1138,16 +1124,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = binary_sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 80e5ce60a47..e9e209358da 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -31,7 +31,6 @@ from .test_common import ( help_test_entity_id_update_discovery_update, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -46,11 +45,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {button.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[button.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def button_platform_only(): @@ -484,15 +478,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = button.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = button.DOMAIN @@ -507,16 +492,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = button.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 4cb6afb6495..20060c196b4 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,6 +1,5 @@ """The tests for mqtt camera component.""" from base64 import b64encode -import copy from http import HTTPStatus import json from unittest.mock import patch @@ -30,7 +29,6 @@ from .test_common import ( help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -45,11 +43,6 @@ from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = {mqtt.DOMAIN: {camera.DOMAIN: {"name": "test", "topic": "test_topic"}}} -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[camera.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def camera_platform_only(): @@ -95,7 +88,7 @@ async def test_run_camera_b64_encoded( camera.DOMAIN: { "topic": topic, "name": "Test Camera", - "encoding": "b64", + "image_encoding": "b64", } } }, @@ -114,44 +107,6 @@ async def test_run_camera_b64_encoded( assert body == "grass" -# Using CONF_ENCODING to set b64 encoding for images is deprecated in Home Assistant 2022.9, use CONF_IMAGE_ENCODING instead -async def test_legacy_camera_b64_encoded_with_availability( - hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config -): - """Test availability works if b64 encoding (legacy mode) is turned on.""" - topic = "test/camera" - topic_availability = "test/camera_availability" - await async_setup_component( - hass, - mqtt.DOMAIN, - { - mqtt.DOMAIN: { - camera.DOMAIN: { - "topic": topic, - "name": "Test Camera", - "encoding": "b64", - "availability": {"topic": topic_availability}, - } - } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - # Make sure we are available - async_fire_mqtt_message(hass, topic_availability, "online") - - url = hass.states.get("camera.test_camera").attributes["entity_picture"] - - async_fire_mqtt_message(hass, topic, b64encode(b"grass")) - - client = await hass_client_no_auth() - resp = await client.get(url) - assert resp.status == HTTPStatus.OK - body = await resp.text() - assert body == "grass" - - async def test_camera_b64_encoded_with_availability( hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config ): @@ -424,15 +379,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = camera.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = camera.DOMAIN @@ -447,16 +393,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = camera.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index d7d278be160..dd86c41dcc7 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -46,7 +46,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -88,11 +87,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[climate.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def climate_platform_only(): @@ -126,7 +120,6 @@ async def test_preset_none_in_preset_modes(hass, caplog): assert "Invalid config for [mqtt]: not a valid value" in caplog.text -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 @pytest.mark.parametrize( "parameter,config_value", [ @@ -1406,15 +1399,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = climate.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = climate.DOMAIN @@ -1429,16 +1413,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = climate.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index a9cfb88bfb6..e5880b981a2 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1705,103 +1705,16 @@ async def help_test_reloadable( old_config_2 = copy.deepcopy(config) old_config_2["name"] = "test_old_2" - # Test deprecated YAML configuration under the platform key - # Scheduled to be removed in HA core 2022.12 - old_config_3 = copy.deepcopy(config) - old_config_3["name"] = "test_old_3" - old_config_3["platform"] = mqtt.DOMAIN - old_config_4 = copy.deepcopy(config) - old_config_4["name"] = "test_old_4" - old_config_4["platform"] = mqtt.DOMAIN - old_config = { mqtt.DOMAIN: {domain: [old_config_1, old_config_2]}, - domain: [old_config_3, old_config_4], } - assert await async_setup_component(hass, domain, old_config) assert await async_setup_component(hass, mqtt.DOMAIN, old_config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_2") - assert hass.states.get(f"{domain}.test_old_3") - assert hass.states.get(f"{domain}.test_old_4") - assert len(hass.states.async_all(domain)) == 4 - - # Create temporary fixture for configuration.yaml based on the supplied config and - # test a reload with this new config - new_config_1 = copy.deepcopy(config) - new_config_1["name"] = "test_new_1" - new_config_2 = copy.deepcopy(config) - new_config_2["name"] = "test_new_2" - new_config_extra = copy.deepcopy(config) - new_config_extra["name"] = "test_new_5" - - # Test deprecated YAML configuration under the platform key - # Scheduled to be removed in HA core 2022.12 - new_config_3 = copy.deepcopy(config) - new_config_3["name"] = "test_new_3" - new_config_3["platform"] = mqtt.DOMAIN - new_config_4 = copy.deepcopy(config) - new_config_4["name"] = "test_new_4" - new_config_4["platform"] = mqtt.DOMAIN - new_config_extra_legacy = copy.deepcopy(config) - new_config_extra_legacy["name"] = "test_new_6" - new_config_extra_legacy["platform"] = mqtt.DOMAIN - - new_config = { - mqtt.DOMAIN: {domain: [new_config_1, new_config_2, new_config_extra]}, - domain: [new_config_3, new_config_4, new_config_extra_legacy], - } - - await help_test_reload_with_config(hass, caplog, tmp_path, new_config) - - assert len(hass.states.async_all(domain)) == 6 - - assert hass.states.get(f"{domain}.test_new_1") - assert hass.states.get(f"{domain}.test_new_2") - assert hass.states.get(f"{domain}.test_new_3") - assert hass.states.get(f"{domain}.test_new_4") - assert hass.states.get(f"{domain}.test_new_5") - assert hass.states.get(f"{domain}.test_new_6") - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): - """Test reloading an MQTT platform when config entry is setup is late.""" - # Create and test an old config of 2 entities based on the config supplied - # using the deprecated platform schema - old_config_1 = copy.deepcopy(config) - old_config_1["name"] = "test_old_1" - old_config_2 = copy.deepcopy(config) - old_config_2["name"] = "test_old_2" - - old_yaml_config_file = tmp_path / "configuration.yaml" - old_yaml_config = yaml.dump({domain: [old_config_1, old_config_2]}) - old_yaml_config_file.write_text(old_yaml_config) - assert old_yaml_config_file.read_text() == old_yaml_config - - assert await async_setup_component( - hass, domain, {domain: [old_config_1, old_config_2]} - ) - await hass.async_block_till_done() - - # No MQTT config entry, there should be a warning and no entities - assert ( - "MQTT integration is not setup, skipping setup of manually " - f"configured MQTT {domain}" - ) in caplog.text - assert len(hass.states.async_all(domain)) == 0 - - # User sets up a config entry, should succeed and entities will setup - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - entry.add_to_hass(hass) - with patch.object(hass_config, "YAML_CONFIG_FILE", old_yaml_config_file): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() assert len(hass.states.async_all(domain)) == 2 # Create temporary fixture for configuration.yaml based on the supplied config and @@ -1810,14 +1723,14 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): new_config_1["name"] = "test_new_1" new_config_2 = copy.deepcopy(config) new_config_2["name"] = "test_new_2" - new_config_3 = copy.deepcopy(config) - new_config_3["name"] = "test_new_3" + new_config_extra = copy.deepcopy(config) + new_config_extra["name"] = "test_new_3" new_config = { - domain: [new_config_1, new_config_2, new_config_3], + mqtt.DOMAIN: {domain: [new_config_1, new_config_2, new_config_extra]}, } + await help_test_reload_with_config(hass, caplog, tmp_path, new_config) - await hass.async_block_till_done() assert len(hass.states.async_all(domain)) == 3 diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index eef728664aa..818cdcf33a6 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -194,6 +194,45 @@ async def test_user_connection_works( assert len(mock_finish_setup.mock_calls) == 1 +async def test_user_v5_connection_works( + hass, mock_try_connection, mock_finish_setup, mqtt_client_mock +): + """Test we can finish a config flow.""" + mock_try_connection.return_value = True + + result = await hass.config_entries.flow.async_init( + "mqtt", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"broker": "127.0.0.1", "advanced_options": True} + ) + + assert result["step_id"] == "broker" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "another-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_PROTOCOL: "5", + }, + ) + assert result["type"] == "create_entry" + assert result["result"].data == { + "broker": "another-broker", + "discovery": True, + "discovery_prefix": "homeassistant", + "port": 2345, + "protocol": "5", + } + # Check we tried the connection + assert len(mock_try_connection.mock_calls) == 1 + # Check config entry got setup + assert len(mock_finish_setup.mock_calls) == 1 + + async def test_user_connection_fails( hass, mock_try_connection_time_out, mock_finish_setup ): @@ -1101,7 +1140,6 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection): async def test_try_connection_with_advanced_parameters( hass, - mqtt_mock_entry_with_yaml_config, mock_try_connection_success, tmp_path, mock_ssl_context, @@ -1132,6 +1170,9 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_PORT: 1234, mqtt.CONF_USERNAME: "user", mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_PATH: "/path/", + mqtt.CONF_WS_HEADERS: {"h1": "v1", "h2": "v2"}, mqtt.CONF_KEEPALIVE: 30, mqtt.CONF_DISCOVERY: True, mqtt.CONF_BIRTH_MESSAGE: { @@ -1166,6 +1207,9 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_PASSWORD: "pass", mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_PROTOCOL: "3.1.1", + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_PATH: "/path/", + mqtt.CONF_WS_HEADERS: '{"h1":"v1","h2":"v2"}', } for k, v in defaults.items(): assert get_default(result["data_schema"].schema, k) == v @@ -1181,7 +1225,7 @@ async def test_try_connection_with_advanced_parameters( ) assert config_entry.data[mqtt.CONF_CERTIFICATE] == "auto" - # test we can chante username and password + # test we can change username and password # as it was configured as auto in configuration.yaml is is migrated now mock_try_connection_success.reset_mock() result = await hass.config_entries.options.async_configure( @@ -1194,6 +1238,9 @@ async def test_try_connection_with_advanced_parameters( "set_ca_cert": "auto", "set_client_cert": True, mqtt.CONF_TLS_INSECURE: True, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_PATH: "/new/path", + mqtt.CONF_WS_HEADERS: '{"h3": "v3"}', }, ) assert result["type"] == data_entry_flow.FlowResultType.FORM @@ -1217,6 +1264,12 @@ async def test_try_connection_with_advanced_parameters( "keyfile" ] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_KEY) + # check if websockets options are set + assert mock_try_connection_success.ws_set_options.mock_calls[0][1] == ( + "/new/path", + {"h3": "v3"}, + ) + # Accept default option result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -1266,6 +1319,7 @@ async def test_setup_with_advanced_settings( assert result["data_schema"].schema["set_ca_cert"] assert result["data_schema"].schema[mqtt.CONF_TLS_INSECURE] assert result["data_schema"].schema[mqtt.CONF_PROTOCOL] + assert result["data_schema"].schema[mqtt.CONF_TRANSPORT] assert mqtt.CONF_CLIENT_CERT not in result["data_schema"].schema assert mqtt.CONF_CLIENT_KEY not in result["data_schema"].schema @@ -1281,6 +1335,8 @@ async def test_setup_with_advanced_settings( "set_ca_cert": "auto", "set_client_cert": True, mqtt.CONF_TLS_INSECURE: True, + mqtt.CONF_PROTOCOL: "3.1.1", + mqtt.CONF_TRANSPORT: "websockets", }, ) assert result["type"] == "form" @@ -1294,8 +1350,11 @@ async def test_setup_with_advanced_settings( assert result["data_schema"].schema[mqtt.CONF_PROTOCOL] assert result["data_schema"].schema[mqtt.CONF_CLIENT_CERT] assert result["data_schema"].schema[mqtt.CONF_CLIENT_KEY] + assert result["data_schema"].schema[mqtt.CONF_TRANSPORT] + assert result["data_schema"].schema[mqtt.CONF_WS_PATH] + assert result["data_schema"].schema[mqtt.CONF_WS_HEADERS] - # third iteration, advanced settings with client cert and key set + # third iteration, advanced settings with client cert and key set and bad json payload result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ @@ -1309,6 +1368,34 @@ async def test_setup_with_advanced_settings( mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT], mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY], mqtt.CONF_TLS_INSECURE: True, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_PATH: "/custom_path/", + mqtt.CONF_WS_HEADERS: '{"header_1": "content_header_1", "header_2": "content_header_2"', + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "broker" + assert result["errors"]["base"] == "bad_ws_headers" + + # fourth iteration, advanced settings with client cert and key set + # and correct json payload for ws_headers + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "secret", + mqtt.CONF_KEEPALIVE: 30, + "set_ca_cert": "auto", + "set_client_cert": True, + mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT], + mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY], + mqtt.CONF_TLS_INSECURE: True, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_PATH: "/custom_path/", + mqtt.CONF_WS_HEADERS: '{"header_1": "content_header_1", "header_2": "content_header_2"}', }, ) @@ -1323,3 +1410,80 @@ async def test_setup_with_advanced_settings( }, ) assert result["type"] == "create_entry" + + # Check config entry result + assert config_entry.data == { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "secret", + mqtt.CONF_KEEPALIVE: 30, + mqtt.CONF_CLIENT_CERT: "## mock client certificate file ##", + mqtt.CONF_CLIENT_KEY: "## mock key file ##", + "tls_insecure": True, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_PATH: "/custom_path/", + mqtt.CONF_WS_HEADERS: { + "header_1": "content_header_1", + "header_2": "content_header_2", + }, + mqtt.CONF_CERTIFICATE: "auto", + mqtt.CONF_DISCOVERY: True, + mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", + } + + +async def test_change_websockets_transport_to_tcp( + hass, mock_try_connection, tmp_path, mock_ssl_context, mock_process_uploaded_file +): + """Test option flow setup with websockets transport settings.""" + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + config_entry.data = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"}, + mqtt.CONF_WS_PATH: "/some_path", + } + + mock_try_connection.return_value = True + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "broker" + assert result["data_schema"].schema["transport"] + assert result["data_schema"].schema["ws_path"] + assert result["data_schema"].schema["ws_headers"] + + # Change transport to tcp + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "tcp", + mqtt.CONF_WS_HEADERS: '{"header_1": "custom_header1"}', + mqtt.CONF_WS_PATH: "/some_path", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_DISCOVERY: True, + mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", + }, + ) + assert result["type"] == "create_entry" + + # Check config entry result + assert config_entry.data == { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "tcp", + mqtt.CONF_DISCOVERY: True, + mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", + } diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index fb7df111d96..93656c6aae3 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,6 +1,5 @@ """The tests for the MQTT cover platform.""" -import copy from unittest.mock import patch import pytest @@ -67,7 +66,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -84,11 +82,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {cover.DOMAIN: {"name": "test", "state_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[cover.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def cover_platform_only(): @@ -3368,15 +3361,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = cover.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -3424,16 +3408,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = cover.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index db6e0a292d7..6db5811afd4 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,22 +1,29 @@ -"""The tests for the MQTT device tracker platform using configuration.yaml with legacy schema.""" -import json +"""The tests for the MQTT device_tracker platform.""" + from unittest.mock import patch import pytest -from homeassistant.components import device_tracker -from homeassistant.components.device_tracker import SourceType -from homeassistant.config_entries import ConfigEntryDisabler -from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform +from homeassistant.components import device_tracker, mqtt +from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, Platform from homeassistant.setup import async_setup_component from .test_common import ( - MockConfigEntry, - help_test_entry_reload_with_new_config, - help_test_unload_config_entry, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, ) -from tests.common import async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry + +DEFAULT_CONFIG = { + mqtt.DOMAIN: { + device_tracker.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + } + } +} @pytest.fixture(autouse=True) @@ -26,380 +33,425 @@ def device_tracker_platform_only(): yield -# Deprecated in HA Core 2022.6 -async def test_legacy_ensure_device_tracker_platform_validation( - hass, mqtt_mock_entry_with_yaml_config -): - """Test if platform validation was done.""" - - async def mock_setup_scanner(hass, config, see, discovery_info=None): - """Check that Qos was added by validation.""" - assert "qos" in config - - with patch( - "homeassistant.components.mqtt.device_tracker.async_setup_scanner", - autospec=True, - side_effect=mock_setup_scanner, - ) as mock_sp: - - dev_id = "paulus" - topic = "/location/paulus" - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: topic}, - } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert mock_sp.call_count == 1 +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) -# Deprecated in HA Core 2022.6 -async def test_legacy_new_message( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test new message.""" +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_discover_device_tracker(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test discovering an MQTT device tracker component.""" await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - topic = "/location/paulus" - location = "work" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - {device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}}, - ) - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == location - - -# Deprecated in HA Core 2022.6 -async def test_legacy_single_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test single level wildcard topic.""" - await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - subscription = "/location/+/paulus" - topic = "/location/room/paulus" - location = "work" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: subscription}, - } - }, - ) - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == location - - -# Deprecated in HA Core 2022.6 -async def test_legacy_multi_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test multi level wildcard topic.""" - await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - subscription = "/location/#" - topic = "/location/room/paulus" - location = "work" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: subscription}, - } - }, - ) - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == location - - -# Deprecated in HA Core 2022.6 -async def test_legacy_single_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test not matching single level wildcard topic.""" - await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - subscription = "/location/+/paulus" - topic = "/location/paulus" - location = "work" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: subscription}, - } - }, - ) - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id) is None - - -# Deprecated in HA Core 2022.6 -async def test_legacy_multi_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test not matching multi level wildcard topic.""" - await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - subscription = "/location/#" - topic = "/somewhere/room/paulus" - location = "work" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: subscription}, - } - }, - ) - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id) is None - - -# Deprecated in HA Core 2022.6 -async def test_legacy_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" - await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - topic = "/location/paulus" - payload_home = "present" - payload_not_home = "not present" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: topic}, - "payload_home": payload_home, - "payload_not_home": payload_not_home, - } - }, - ) - async_fire_mqtt_message(hass, topic, payload_home) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_HOME - - async_fire_mqtt_message(hass, topic, payload_not_home) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_NOT_HOME - - -# Deprecated in HA Core 2022.6 -async def test_legacy_not_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test not matching payload does not set state to home or not_home.""" - await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - topic = "/location/paulus" - payload_home = "present" - payload_not_home = "not present" - payload_not_matching = "test" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: topic}, - "payload_home": payload_home, - "payload_not_home": payload_not_home, - } - }, - ) - async_fire_mqtt_message(hass, topic, payload_not_matching) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state != STATE_HOME - assert hass.states.get(entity_id).state != STATE_NOT_HOME - - -# Deprecated in HA Core 2022.6 -async def test_legacy_matching_source_type( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config -): - """Test setting source type.""" - await mqtt_mock_entry_no_yaml_config() - dev_id = "paulus" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - topic = "/location/paulus" - source_type = SourceType.BLUETOOTH - location = "work" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - { - device_tracker.DOMAIN: { - CONF_PLATFORM: "mqtt", - "devices": {dev_id: topic}, - "source_type": source_type, - } - }, - ) - - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id).attributes["source_type"] == SourceType.BLUETOOTH - - -# Deprecated in HA Core 2022.6 -async def test_unload_entry( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path -): - """Test unloading the config entry.""" - # setup through configuration.yaml - await mqtt_mock_entry_no_yaml_config() - dev_id = "jan" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - topic = "/location/jan" - location = "home" - - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component( - hass, - device_tracker.DOMAIN, - {device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}}, - ) - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == location - - # setup through discovery - dev_id = "piet" - subscription = "/location/#" - domain = device_tracker.DOMAIN - discovery_config = { - "devices": {dev_id: subscription}, - "state_topic": "some-state", - "name": "piet", - } async_fire_mqtt_message( - hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_config) + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test_topic" }', ) await hass.async_block_till_done() - # check that both entities were created - config_setup_entity = hass.states.get(f"{domain}.jan") - assert config_setup_entity + state = hass.states.get("device_tracker.test") - discovery_setup_entity = hass.states.get(f"{domain}.piet") - assert discovery_setup_entity - - await help_test_unload_config_entry(hass, tmp_path, {}) - await hass.async_block_till_done() - - # check that both entities were unsubscribed and that the location was not processed - async_fire_mqtt_message(hass, "some-state", "not_home") - async_fire_mqtt_message(hass, "location/jan", "not_home") - await hass.async_block_till_done() - - config_setup_entity = hass.states.get(f"{domain}.jan") - assert config_setup_entity.state == location - - # the discovered tracker is an entity which state is removed at unload - discovery_setup_entity = hass.states.get(f"{domain}.piet") - assert discovery_setup_entity is None + assert state is not None + assert state.name == "test" + assert ("device_tracker", "bla") in hass.data["mqtt"].discovery_already_discovered -# Deprecated in HA Core 2022.6 -async def test_reload_entry_legacy( - hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path -): - """Test reloading the config entry with manual MQTT items.""" - # setup through configuration.yaml +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test handling of bad discovery message.""" await mqtt_mock_entry_no_yaml_config() - entity_id = f"{device_tracker.DOMAIN}.jan" - topic = "location/jan" - location = "home" - - config = { - device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, - } - hass.config.components = {"mqtt", "zone"} - assert await async_setup_component(hass, device_tracker.DOMAIN, config) - await hass.async_block_till_done() - - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == location - - await help_test_entry_reload_with_new_config(hass, tmp_path, config) - await hass.async_block_till_done() - - location = "not_home" - async_fire_mqtt_message(hass, topic, location) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == location - - -# Deprecated in HA Core 2022.6 -async def test_setup_with_disabled_entry( - hass, mock_device_tracker_conf, caplog -) -> None: - """Test setting up the platform with a disabled config entry.""" - # Try to setup the platform with a disabled config entry - config_entry = MockConfigEntry( - domain="mqtt", data={}, disabled_by=ConfigEntryDisabler.USER + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer" }', ) - config_entry.add_to_hass(hass) - topic = "location/jan" + await hass.async_block_till_done() - config = { - device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, - } - hass.config.components = {"mqtt", "zone"} + state = hass.states.get("device_tracker.beer") + assert state is None - await async_setup_component(hass, device_tracker.DOMAIN, config) + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "required-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Beer" + + +async def test_non_duplicate_device_tracker_discovery( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + state_duplicate = hass.states.get("device_tracker.beer1") + + assert state is not None + assert state.name == "Beer" + assert state_duplicate is None + assert "Component has already been discovered: device_tracker bla" in caplog.text + + +async def test_device_tracker_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test removal of component through empty discovery message.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is None + + +async def test_device_tracker_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test rediscover of removed component.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is None + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + +async def test_duplicate_device_tracker_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + assert "Component has already been discovered: device_tracker bla" in caplog.text + caplog.clear() + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") await hass.async_block_till_done() assert ( - "MQTT device trackers will be not available until the config entry is enabled" - in caplog.text + "Component has already been discovered: device_tracker bla" not in caplog.text ) + + +async def test_device_tracker_discovery_update( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test for a discovery update event.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Beer" + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Cider", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Cider" + + +async def test_cleanup_device_tracker( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): + """Test discovered device is cleaned up when removed from registry.""" + assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + ws_client = await hass_ws_client(hass) + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/tracker",' + ' "unique_id": "unique" }', + ) + await hass.async_block_till_done() + + # Verify device and registry entries are created + 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 + + state = hass.states.get("device_tracker.mqtt_unique") + assert state is not None + + # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] + await ws_client.send_json( + { + "id": 6, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify device and registry entries are cleared + 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 + + # Verify state is removed + state = hass.states.get("device_tracker.mqtt_unique") + assert state is None + await hass.async_block_till_done() + + # Verify retained discovery topic has been cleared + mqtt_mock.async_publish.assert_called_once_with( + "homeassistant/device_tracker/bla/config", "", 0, True + ) + + +async def test_setting_device_tracker_value_via_mqtt_message( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test-topic" }', + ) + + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "not_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_value_via_mqtt_message_and_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{" + '"name": "test", ' + '"state_topic": "test-topic", ' + '"value_template": "{% if value is equalto \\"proxy_for_home\\" %}home{% else %}not_home{% endif %}" ' + "}", + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test-topic", "proxy_for_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "anything_for_not_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_value_via_mqtt_message_and_template2( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{" + '"name": "test", ' + '"state_topic": "test-topic", ' + '"value_template": "{{ value | lower }}" ' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "HOME") + state = hass.states.get("device_Tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "NOT_HOME") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_location_via_mqtt_message( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test the setting of the location via MQTT.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test-topic", "source_type": "router" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + assert state.attributes["source_type"] == "router" + + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "test-location") + state = hass.states.get("device_tracker.test") + assert state.state == "test-location" + + +async def test_setting_device_tracker_location_via_lat_lon_message( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test the setting of the latitude and longitude via MQTT.""" + await mqtt_mock_entry_no_yaml_config() + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{ " + '"name": "test", ' + '"state_topic": "test-topic", ' + '"json_attributes_topic": "attributes-topic" ' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + assert state.attributes["source_type"] == "gps" + + assert state.state == STATE_UNKNOWN + + hass.config.latitude = 32.87336 + hass.config.longitude = -117.22743 + + async_fire_mqtt_message( + hass, + "attributes-topic", + '{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5, "source_type": "router"}', + ) + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 32.87336 + assert state.attributes["longitude"] == -117.22743 + assert state.attributes["gps_accuracy"] == 1.5 + # assert source_type is overridden by discovery + assert state.attributes["source_type"] == "router" + assert state.state == STATE_HOME + + async_fire_mqtt_message( + hass, + "attributes-topic", + '{"latitude":50.1,"longitude": -2.1}', + ) + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 50.1 + assert state.attributes["longitude"] == -2.1 + assert state.attributes["gps_accuracy"] == 0 + assert state.state == STATE_NOT_HOME + + async_fire_mqtt_message(hass, "attributes-topic", '{"longitude": -117.22743}') + state = hass.states.get("device_tracker.test") + assert state.attributes["longitude"] == -117.22743 + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "attributes-topic", '{"latitude":32.87336}') + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 32.87336 + assert state.state == STATE_UNKNOWN + + +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, + mqtt_mock_entry_no_yaml_config, + device_tracker.DOMAIN, + DEFAULT_CONFIG, + None, + ) + + +async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): + """Test setup using the modern schema.""" + dev_id = "jan" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" + topic = "/location/jan" + + config = { + mqtt.DOMAIN: {device_tracker.DOMAIN: {"name": dev_id, "state_topic": topic}} + } + + await help_test_setup_manual_entity_from_yaml(hass, config) + + assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py deleted file mode 100644 index a39b9b696c5..00000000000 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ /dev/null @@ -1,453 +0,0 @@ -"""The tests for the MQTT device_tracker platform.""" - -from unittest.mock import patch - -import pytest - -from homeassistant.components import device_tracker, mqtt -from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, Platform -from homeassistant.setup import async_setup_component - -from .test_common import ( - help_test_setting_blocked_attribute_via_mqtt_json_message, - help_test_setup_manual_entity_from_yaml, -) - -from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry - -DEFAULT_CONFIG = { - mqtt.DOMAIN: { - device_tracker.DOMAIN: { - "name": "test", - "state_topic": "test-topic", - } - } -} - - -@pytest.fixture(autouse=True) -def device_tracker_platform_only(): - """Only setup the device_tracker platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.DEVICE_TRACKER]): - yield - - -@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_discover_device_tracker(hass, mqtt_mock_entry_no_yaml_config, caplog): - """Test discovering an MQTT device tracker component.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "test", "state_topic": "test_topic" }', - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.test") - - assert state is not None - assert state.name == "test" - assert ("device_tracker", "bla") in hass.data["mqtt"].discovery_already_discovered - - -@pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): - """Test handling of bad discovery message.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer" }', - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.beer") - assert state is None - - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "required-topic" }', - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.beer") - assert state is not None - assert state.name == "Beer" - - -async def test_non_duplicate_device_tracker_discovery( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test for a non duplicate component.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', - ) - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.beer") - state_duplicate = hass.states.get("device_tracker.beer1") - - assert state is not None - assert state.name == "Beer" - assert state_duplicate is None - assert "Component has already been discovered: device_tracker bla" in caplog.text - - -async def test_device_tracker_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): - """Test removal of component through empty discovery message.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - state = hass.states.get("device_tracker.beer") - assert state is not None - - async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") - await hass.async_block_till_done() - state = hass.states.get("device_tracker.beer") - assert state is None - - -async def test_device_tracker_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): - """Test rediscover of removed component.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - state = hass.states.get("device_tracker.beer") - assert state is not None - - async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") - await hass.async_block_till_done() - state = hass.states.get("device_tracker.beer") - assert state is None - - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - state = hass.states.get("device_tracker.beer") - assert state is not None - - -async def test_duplicate_device_tracker_removal( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test for a non duplicate component.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") - await hass.async_block_till_done() - assert "Component has already been discovered: device_tracker bla" in caplog.text - caplog.clear() - async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") - await hass.async_block_till_done() - - assert ( - "Component has already been discovered: device_tracker bla" not in caplog.text - ) - - -async def test_device_tracker_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test for a discovery update event.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Beer", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.beer") - assert state is not None - assert state.name == "Beer" - - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "Cider", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.beer") - assert state is not None - assert state.name == "Cider" - - -async def test_cleanup_device_tracker( - hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config -): - """Test discovered device is cleaned up when removed from registry.""" - assert await async_setup_component(hass, "config", {}) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_no_yaml_config() - ws_client = await hass_ws_client(hass) - - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "device":{"identifiers":["0AFFD2"]},' - ' "state_topic": "foobar/tracker",' - ' "unique_id": "unique" }', - ) - await hass.async_block_till_done() - - # Verify device and registry entries are created - 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 - - state = hass.states.get("device_tracker.mqtt_unique") - assert state is not None - - # Remove MQTT from the device - mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] - await ws_client.send_json( - { - "id": 6, - "type": "config/device_registry/remove_config_entry", - "config_entry_id": mqtt_config_entry.entry_id, - "device_id": device_entry.id, - } - ) - response = await ws_client.receive_json() - assert response["success"] - await hass.async_block_till_done() - await hass.async_block_till_done() - - # Verify device and registry entries are cleared - 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 - - # Verify state is removed - state = hass.states.get("device_tracker.mqtt_unique") - assert state is None - await hass.async_block_till_done() - - # Verify retained discovery topic has been cleared - mqtt_mock.async_publish.assert_called_once_with( - "homeassistant/device_tracker/bla/config", "", 0, True - ) - - -async def test_setting_device_tracker_value_via_mqtt_message( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test the setting of the value via MQTT.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "test", "state_topic": "test-topic" }', - ) - - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.test") - - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, "test-topic", "home") - state = hass.states.get("device_tracker.test") - assert state.state == STATE_HOME - - async_fire_mqtt_message(hass, "test-topic", "not_home") - state = hass.states.get("device_tracker.test") - assert state.state == STATE_NOT_HOME - - -async def test_setting_device_tracker_value_via_mqtt_message_and_template( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test the setting of the value via MQTT.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - "{" - '"name": "test", ' - '"state_topic": "test-topic", ' - '"value_template": "{% if value is equalto \\"proxy_for_home\\" %}home{% else %}not_home{% endif %}" ' - "}", - ) - await hass.async_block_till_done() - - async_fire_mqtt_message(hass, "test-topic", "proxy_for_home") - state = hass.states.get("device_tracker.test") - assert state.state == STATE_HOME - - async_fire_mqtt_message(hass, "test-topic", "anything_for_not_home") - state = hass.states.get("device_tracker.test") - assert state.state == STATE_NOT_HOME - - -async def test_setting_device_tracker_value_via_mqtt_message_and_template2( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test the setting of the value via MQTT.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - "{" - '"name": "test", ' - '"state_topic": "test-topic", ' - '"value_template": "{{ value | lower }}" ' - "}", - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.test") - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, "test-topic", "HOME") - state = hass.states.get("device_Tracker.test") - assert state.state == STATE_HOME - - async_fire_mqtt_message(hass, "test-topic", "NOT_HOME") - state = hass.states.get("device_tracker.test") - assert state.state == STATE_NOT_HOME - - -async def test_setting_device_tracker_location_via_mqtt_message( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test the setting of the location via MQTT.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - '{ "name": "test", "state_topic": "test-topic" }', - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.test") - - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, "test-topic", "test-location") - state = hass.states.get("device_tracker.test") - assert state.state == "test-location" - - -async def test_setting_device_tracker_location_via_lat_lon_message( - hass, mqtt_mock_entry_no_yaml_config, caplog -): - """Test the setting of the latitude and longitude via MQTT.""" - await mqtt_mock_entry_no_yaml_config() - async_fire_mqtt_message( - hass, - "homeassistant/device_tracker/bla/config", - "{ " - '"name": "test", ' - '"state_topic": "test-topic", ' - '"json_attributes_topic": "attributes-topic" ' - "}", - ) - await hass.async_block_till_done() - - state = hass.states.get("device_tracker.test") - - assert state.state == STATE_UNKNOWN - - hass.config.latitude = 32.87336 - hass.config.longitude = -117.22743 - - async_fire_mqtt_message( - hass, - "attributes-topic", - '{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5}', - ) - state = hass.states.get("device_tracker.test") - assert state.attributes["latitude"] == 32.87336 - assert state.attributes["longitude"] == -117.22743 - assert state.attributes["gps_accuracy"] == 1.5 - assert state.state == STATE_HOME - - async_fire_mqtt_message( - hass, - "attributes-topic", - '{"latitude":50.1,"longitude": -2.1, "gps_accuracy":1.5}', - ) - state = hass.states.get("device_tracker.test") - assert state.attributes["latitude"] == 50.1 - assert state.attributes["longitude"] == -2.1 - assert state.attributes["gps_accuracy"] == 1.5 - assert state.state == STATE_NOT_HOME - - async_fire_mqtt_message(hass, "attributes-topic", '{"longitude": -117.22743}') - state = hass.states.get("device_tracker.test") - assert state.attributes["longitude"] == -117.22743 - assert state.state == STATE_UNKNOWN - - async_fire_mqtt_message(hass, "attributes-topic", '{"latitude":32.87336}') - state = hass.states.get("device_tracker.test") - assert state.attributes["latitude"] == 32.87336 - assert state.state == STATE_UNKNOWN - - -async def test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config -): - """Test the setting of attribute via MQTT with JSON payload.""" - await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, - mqtt_mock_entry_no_yaml_config, - device_tracker.DOMAIN, - DEFAULT_CONFIG, - None, - ) - - -async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): - """Test setup using the modern schema.""" - dev_id = "jan" - entity_id = f"{device_tracker.DOMAIN}.{dev_id}" - topic = "/location/jan" - - config = { - mqtt.DOMAIN: {device_tracker.DOMAIN: {"name": dev_id, "state_topic": topic}} - } - - await help_test_setup_manual_entity_from_yaml(hass, config) - - assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index 7c486f9af74..7512a46802f 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -23,6 +23,9 @@ default_config = { "port": 1883, "protocol": "3.1.1", "tls_version": "auto", + "transport": "tcp", + "ws_headers": {}, + "ws_path": "/", "will_message": { "payload": "offline", "qos": 0, @@ -248,7 +251,7 @@ async def test_redact_diagnostics( "gps_accuracy": 1.5, "latitude": "**REDACTED**", "longitude": "**REDACTED**", - "source_type": None, + "source_type": "gps", }, "entity_id": "device_tracker.mqtt_unique", "last_changed": ANY, diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 77d7093830e..89a56903c3b 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1179,7 +1179,10 @@ ABBREVIATIONS_WHITE_LIST = [ "CONF_KEEPALIVE", "CONF_TLS_INSECURE", "CONF_TLS_VERSION", + "CONF_TRANSPORT", "CONF_WILL_MESSAGE", + "CONF_WS_PATH", + "CONF_WS_HEADERS", # Undocumented device configuration "CONF_DEPRECATED_VIA_HUB", "CONF_VIA_DEVICE", @@ -1235,7 +1238,6 @@ async def test_no_implicit_state_topic_switch( async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) await hass.async_block_till_done() - assert "implicit state_topic is deprecated" not in caplog.text state = hass.states.get("switch.Test1") assert state is not None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 1666ccee6ce..f4e89aa1ceb 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -52,7 +52,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -76,11 +75,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[fan.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def fan_platform_only(): @@ -1941,15 +1935,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = fan.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = fan.DOMAIN @@ -1964,16 +1949,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 1e2d64b66cf..1b8eac397cf 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -54,7 +54,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -78,11 +77,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[humidifier.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def humidifer_platform_only(): @@ -1313,15 +1307,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = humidifier.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = humidifier.DOMAIN @@ -1347,16 +1332,3 @@ async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_p await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 426ccb5806f..ced93a8c997 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -882,6 +882,7 @@ async def test_subscribe_bad_topic( await mqtt.async_subscribe(hass, 55, record_calls) +# Support for a deprecated callback type will be removed from HA core 2023.2.0 async def test_subscribe_deprecated(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated callback signature.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() @@ -930,6 +931,7 @@ async def test_subscribe_deprecated(hass, mqtt_mock_entry_no_yaml_config): assert len(calls) == 1 +# Support for a deprecated callback type will be removed from HA core 2023.2.0 async def test_subscribe_deprecated_async(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated coroutine signature.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() @@ -2718,8 +2720,8 @@ async def test_subscribe_connection_status( assert mqtt_connected_calls[1] is False -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 +# Test existence of removed YAML configuration under the platform key +# This warning and test is to be removed from HA core 2023.6 async def test_one_deprecation_warning_per_platform( hass, mqtt_mock_entry_with_yaml_config, caplog ): @@ -2735,7 +2737,7 @@ async def test_one_deprecation_warning_per_platform( await mqtt_mock_entry_with_yaml_config() count = 0 for record in caplog.records: - if record.levelname == "WARNING" and ( + if record.levelname == "ERROR" and ( f"Manually configured MQTT {platform}(s) found under platform key '{platform}'" in record.message ): @@ -2808,8 +2810,6 @@ async def test_publish_or_subscribe_without_valid_config_entry(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_reload_entry_with_new_config(hass, tmp_path): """Test reloading the config entry with a new yaml config.""" - # Test deprecated YAML configuration under the platform key - # Scheduled to be removed in HA core 2022.12 config_old = { "mqtt": {"light": [{"name": "test_old1", "command_topic": "test-topic_old"}]} } @@ -2817,15 +2817,6 @@ async def test_reload_entry_with_new_config(hass, tmp_path): "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] }, - # Test deprecated YAML configuration under the platform key - # Scheduled to be removed in HA core 2022.12 - "light": [ - { - "platform": "mqtt", - "name": "test_new_legacy", - "command_topic": "test-topic_new", - } - ], } await help_test_setup_manual_entity_from_yaml(hass, config_old) assert hass.states.get("light.test_old1") is not None @@ -2833,7 +2824,6 @@ async def test_reload_entry_with_new_config(hass, tmp_path): await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) assert hass.states.get("light.test_old1") is None assert hass.states.get("light.test_new_modern") is not None - assert hass.states.get("light.test_new_legacy") is not None @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) @@ -2846,15 +2836,6 @@ async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] }, - # Test deprecated YAML configuration under the platform key - # Scheduled to be removed in HA core 2022.12 - "light": [ - { - "platform": "mqtt", - "name": "test_new_legacy", - "command_topic": "test-topic_new", - } - ], } await help_test_setup_manual_entity_from_yaml(hass, config_old) assert hass.states.get("light.test_old1") is not None @@ -2883,12 +2864,6 @@ async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): await hass.async_block_till_done() await hass.async_block_till_done() - # Assert that the discovery was still received - # but kipped the setup - assert ( - "MQTT integration is disabled, skipping setup of manually configured MQTT light" - in caplog.text - ) assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED assert hass.states.get("light.test_old1") is None @@ -2903,7 +2878,6 @@ async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): assert hass.states.get("light.test_old1") is None assert hass.states.get("light.test_new_modern") is not None - assert hass.states.get("light.test_new_legacy") is not None @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 4b44807b7c9..c9842b09eb2 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -29,7 +29,7 @@ from homeassistant.components.vacuum import ( ATTR_STATUS, VacuumEntityFeature, ) -from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_OFF, STATE_ON, Platform +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, Platform from homeassistant.setup import async_setup_component from .test_common import ( @@ -52,7 +52,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -91,11 +90,6 @@ DEFAULT_CONFIG = { DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def vacuum_platform_only(): @@ -936,15 +930,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = vacuum.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -1011,16 +996,3 @@ async def test_setup_manual_entity_from_yaml(hass): platform = vacuum.DOMAIN await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) assert hass.states.get(f"{platform}.mqtttest") - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 05f9d0e72f0..2404d8a0f1f 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -216,7 +216,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -234,11 +233,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {light.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def light_platform_only(): @@ -2222,11 +2216,12 @@ async def test_discovery_removal_light(hass, mqtt_mock_entry_no_yaml_config, cap ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_discovery_deprecated(hass, mqtt_mock_entry_no_yaml_config, caplog): - """Test discovery of mqtt light with deprecated platform option.""" +async def test_discovery_ignores_extra_keys( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test discovery ignores extra keys that are not blocked.""" await mqtt_mock_entry_no_yaml_config() + # inserted `platform` key should be ignored data = ( '{ "name": "Beer",' ' "platform": "mqtt",' ' "command_topic": "test_topic"}' ) @@ -2945,15 +2940,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value,init_payload", [ @@ -3151,16 +3137,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index f2835121b86..8af7b244f61 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -119,7 +119,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -142,11 +141,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def light_platform_only(): @@ -2205,15 +2199,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value,init_payload", [ @@ -2267,16 +2252,3 @@ async def test_setup_manual_entity_from_yaml(hass): platform = light.DOMAIN await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) assert hass.states.get(f"{platform}.test") - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index dd3f267b0d0..727949eba4b 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -64,7 +64,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -90,11 +89,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def light_platform_only(): @@ -1201,15 +1195,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value,init_payload", [ @@ -1257,16 +1242,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 0784065ddbe..ef1690221aa 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1,5 +1,4 @@ """The tests for the MQTT lock platform.""" -import copy from unittest.mock import patch import pytest @@ -42,7 +41,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -59,11 +57,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {lock.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[lock.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def lock_platform_only(): @@ -726,15 +719,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = lock.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -778,16 +762,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = lock.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 65c8655472c..e547e8207a7 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -1,5 +1,4 @@ """The tests for mqtt number component.""" -import copy import json from unittest.mock import patch @@ -52,7 +51,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -69,11 +67,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {number.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[number.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def number_platform_only(): @@ -907,15 +900,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = number.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -960,16 +944,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = number.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 122c32caa03..7f83ccb5cc2 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -19,7 +19,6 @@ from .test_common import ( help_test_discovery_update, help_test_discovery_update_unchanged, help_test_reloadable, - help_test_reloadable_late, help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_unload_config_entry_with_platform, @@ -37,11 +36,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[scene.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def scene_platform_only(): @@ -231,15 +225,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = scene.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = scene.DOMAIN @@ -254,16 +239,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = scene.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 5b588304061..e82bd20aa2b 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -42,7 +42,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -65,11 +64,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[select.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def select_platform_only(): @@ -661,15 +655,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = select.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -716,16 +701,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = select.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 1884d04efc3..750a6d79edd 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -52,7 +52,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_reload_with_config, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -73,11 +72,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {sensor.DOMAIN: {"name": "test", "state_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[sensor.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def sensor_platform_only(): @@ -1110,15 +1104,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - async def test_cleanup_triggers_and_restoring_state( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, freezer ): @@ -1258,16 +1243,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 47e52cca643..361a043ed4b 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -39,7 +39,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -56,11 +55,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {siren.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[siren.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def siren_platform_only(): @@ -954,15 +948,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = siren.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -1006,16 +991,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 917c5fa43d8..89c3db9b5d9 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -26,13 +26,7 @@ from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_DOCKED, ) -from homeassistant.const import ( - CONF_NAME, - CONF_PLATFORM, - ENTITY_MATCH_ALL, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN, Platform from homeassistant.setup import async_setup_component from .test_common import ( @@ -55,7 +49,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -88,13 +81,6 @@ DEFAULT_CONFIG = { DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"schema": "state", "name": "test"}}} -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN -DEFAULT_CONFIG_2_LEGACY = deepcopy(DEFAULT_CONFIG_2[mqtt.DOMAIN]) -DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def vacuum_platform_only(): @@ -680,15 +666,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = vacuum.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -735,16 +712,3 @@ async def test_setup_manual_entity_from_yaml(hass): platform = vacuum.DOMAIN await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) assert hass.states.get(f"{platform}.mqtttest") - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index 3be66f0aa90..6487e5cb826 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -139,7 +139,6 @@ async def test_qos_encoding_default(hass, mqtt_mock_entry_no_yaml_config, caplog @callback def msg_callback(*args): """Do nothing.""" - pass sub_state = None sub_state = async_prepare_subscribe_topics( @@ -158,7 +157,6 @@ async def test_qos_encoding_custom(hass, mqtt_mock_entry_no_yaml_config, caplog) @callback def msg_callback(*args): """Do nothing.""" - pass sub_state = None sub_state = async_prepare_subscribe_topics( diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 72a09529242..6248bb129aa 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -36,7 +36,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_publishing_with_custom_encoding, help_test_reloadable, - help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -54,11 +53,6 @@ DEFAULT_CONFIG = { mqtt.DOMAIN: {switch.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[switch.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def switch_platform_only(): @@ -642,15 +636,6 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa ) -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): - """Test reloading the MQTT platform with late entry setup.""" - domain = switch.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) - - @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ @@ -694,16 +679,3 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) - - -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): - """Test a setup with deprecated yaml platform schema.""" - domain = switch.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["name"] = "test" - assert await async_setup_component(hass, domain, {domain: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py new file mode 100644 index 00000000000..6b4680bd030 --- /dev/null +++ b/tests/components/mqtt/test_text.py @@ -0,0 +1,674 @@ +"""The tests for the MQTT text platform.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from homeassistant.components import mqtt, text +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +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_encoding_subscribable_topics, + 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_publishing_with_custom_encoding, + help_test_reloadable, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, + help_test_unique_id, + help_test_unload_config_entry_with_platform, + 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 = { + mqtt.DOMAIN: {text.DOMAIN: {"name": "test", "command_topic": "test-topic"}} +} + + +@pytest.fixture(autouse=True) +def text_platform_only(): + """Only setup the text platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.TEXT]): + yield + + +async def async_set_value( + hass: HomeAssistant, entity_id: str, value: str | None +) -> None: + """Set input_text to value.""" + await hass.services.async_call( + text.DOMAIN, + text.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, text.ATTR_VALUE: value}, + blocking=True, + ) + + +async def test_controlling_state_via_topic( + hass: HomeAssistant, mqtt_mock_entry_with_yaml_config +) -> None: + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "mode": "password", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("text.test") + assert state.state == STATE_UNKNOWN + assert state.attributes[text.ATTR_MODE] == "password" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "some state") + + state = hass.states.get("text.test") + assert state.state == "some state" + + async_fire_mqtt_message(hass, "state-topic", "some other state") + + state = hass.states.get("text.test") + assert state.state == "some other state" + + async_fire_mqtt_message(hass, "state-topic", "") + + state = hass.states.get("text.test") + assert state.state == "" + + +async def test_controlling_validation_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config +) -> None: + """Test the validation of a received state.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "mode": "text", + "min": 2, + "max": 10, + "pattern": "(y|n)", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("text.test") + assert state.state == STATE_UNKNOWN + assert state.attributes[text.ATTR_MODE] == "text" + + async_fire_mqtt_message(hass, "state-topic", "yes") + state = hass.states.get("text.test") + assert state.state == "yes" + + # test pattern error + with pytest.raises(ValueError): + async_fire_mqtt_message(hass, "state-topic", "other") + await hass.async_block_till_done() + state = hass.states.get("text.test") + assert state.state == "yes" + + # test text size to large + with pytest.raises(ValueError): + async_fire_mqtt_message(hass, "state-topic", "yesyesyesyes") + await hass.async_block_till_done() + state = hass.states.get("text.test") + assert state.state == "yes" + + # test text size to small + with pytest.raises(ValueError): + async_fire_mqtt_message(hass, "state-topic", "y") + await hass.async_block_till_done() + state = hass.states.get("text.test") + assert state.state == "yes" + + async_fire_mqtt_message(hass, "state-topic", "no") + await hass.async_block_till_done() + state = hass.states.get("text.test") + assert state.state == "no" + + +async def test_attribute_validation_max_greater_then_min(hass) -> None: + """Test the validation of min and max configuration attributes.""" + assert not await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "min": 20, + "max": 10, + } + } + }, + ) + + +async def test_attribute_validation_max_not_greater_then_max_state_length(hass) -> None: + """Test the max value of of max configuration attribute.""" + assert not await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "min": 20, + "max": 257, + } + } + }, + ) + + +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): + """Test the sending MQTT commands in optimistic mode.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": "2", + } + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("text.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_set_value(hass, "text.test", "some other state") + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "some other state", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("text.test") + assert state.state == "some other state" + + await async_set_value(hass, "text.test", "some new state") + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "some new state", 2, False + ) + state = hass.states.get("text.test") + assert state.state == "some new state" + + +async def test_set_text_validation(hass, mqtt_mock_entry_with_yaml_config): + """Test the initial state in optimistic mode.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "mode": "text", + "min": 2, + "max": 10, + "pattern": "(y|n)", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("text.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ASSUMED_STATE) + + # text too long + with pytest.raises(ValueError): + await async_set_value(hass, "text.test", "yes yes yes yes") + + # text too short + with pytest.raises(ValueError): + await async_set_value(hass, "text.test", "y") + + # text not matching pattern + with pytest.raises(ValueError): + await async_set_value(hass, "text.test", "other") + + await async_set_value(hass, "text.test", "no") + state = hass.states.get("text.test") + assert state.state == "no" + + +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock_entry_with_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): + """Test availability by default payload with defined topic.""" + config = { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + } + } + } + + await help_test_default_availability_payload( + hass, + mqtt_mock_entry_with_yaml_config, + text.DOMAIN, + config, + True, + "state-topic", + "some state", + ) + + +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): + """Test availability by custom payload with defined topic.""" + config = { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + } + } + } + + await help_test_custom_availability_payload( + hass, + mqtt_mock_entry_with_yaml_config, + text.DOMAIN, + config, + True, + "state-topic", + "1", + ) + + +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config, text.DOMAIN, DEFAULT_CONFIG, {} + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock_entry_with_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + text.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_json( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + text.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + text.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): + """Test unique id option only creates one text per unique_id.""" + config = { + mqtt.DOMAIN: { + text.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + } + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, text.DOMAIN, config + ) + + +async def test_discovery_removal_text(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test removal of discovered text entity.""" + data = ( + '{ "name": "test",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, text.DOMAIN, data + ) + + +async def test_discovery_text_update(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test update of discovered text entity.""" + config1 = { + "name": "Beer", + "command_topic": "command-topic", + "state_topic": "state-topic", + } + config2 = { + "name": "Milk", + "command_topic": "command-topic", + "state_topic": "state-topic", + } + + await help_test_discovery_update( + hass, mqtt_mock_entry_no_yaml_config, caplog, text.DOMAIN, config1, config2 + ) + + +async def test_discovery_update_unchanged_update( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test update of discovered update.""" + data1 = '{ "name": "Beer", "state_topic": "text-topic", "command_topic": "command-topic"}' + with patch( + "homeassistant.components.mqtt.text.MqttTextEntity.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + text.DOMAIN, + data1, + discovery_update, + ) + + +async def test_discovery_update_text(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test update of discovered text entity.""" + config1 = {"name": "Beer", "command_topic": "cmd-topic1"} + config2 = {"name": "Milk", "command_topic": "cmd-topic2"} + await help_test_discovery_update( + hass, mqtt_mock_entry_no_yaml_config, caplog, text.DOMAIN, config1, config2 + ) + + +async def test_discovery_update_unchanged_climate( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test update of discovered text entity.""" + data1 = '{ "name": "Beer", "command_topic": "cmd-topic" }' + with patch( + "homeassistant.components.mqtt.text.MqttTextEntity.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + text.DOMAIN, + data1, + discovery_update, + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, 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_entry_no_yaml_config, caplog, text.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): + """Test MQTT text device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry_no_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): + """Test MQTT text device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry_no_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock_entry_no_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock_entry_no_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock_entry_with_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock_entry_no_yaml_config, text.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock_entry_no_yaml_config, text.DOMAIN, DEFAULT_CONFIG, None + ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + text.SERVICE_SET_VALUE, + "command_topic", + {text.ATTR_VALUE: "some text"}, + "some text", + "command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = text.DOMAIN + config = DEFAULT_CONFIG + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) + + +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): + """Test reloading the MQTT platform.""" + domain = text.DOMAIN + config = DEFAULT_CONFIG + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) + + +@pytest.mark.parametrize( + "topic,value,attribute,attribute_value", + [ + ("state_topic", "some text", None, "some text"), + ], +) +async def test_encoding_subscribable_topics( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, +): + """Test handling of incoming encoded payload.""" + await help_test_encoding_subscribable_topics( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + text.DOMAIN, + DEFAULT_CONFIG[mqtt.DOMAIN][text.DOMAIN], + topic, + value, + attribute, + attribute_value, + ) + + +async def test_setup_manual_entity_from_yaml(hass): + """Test setup manual configured MQTT entity.""" + platform = text.DOMAIN + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = text.DOMAIN + config = DEFAULT_CONFIG + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index 4c0a70707eb..bebf319d36f 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -194,7 +194,7 @@ async def test_non_allowed_templates(hass, calls, caplog): ) assert ( - "Got error 'TemplateError: str: Use of 'states' is not supported in limited templates' when setting up triggers" + "Got error 'TemplateError: Use of 'states' is not supported in limited templates' when setting up triggers" in caplog.text ) diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index a8f925bf4a6..a633b99fb1a 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -30,6 +30,7 @@ from .test_common import ( help_test_entity_device_info_with_connection, help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, + help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setup_manual_entity_from_yaml, @@ -109,6 +110,54 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("latest_version") == "2.0.0" +async def test_run_update_setup_float(hass, mqtt_mock_entry_with_yaml_config): + """Test that it fetches the given payload when the version is parsable as a number.""" + installed_version_topic = "test/installed-version" + latest_version_topic = "test/latest-version" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": installed_version_topic, + "latest_version_topic": latest_version_topic, + "name": "Test Update", + "release_summary": "Test release summary", + "release_url": "https://example.com/release", + "title": "Test Update Title", + "entity_picture": "https://example.com/icon.png", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, installed_version_topic, "1.9") + async_fire_mqtt_message(hass, latest_version_topic, "1.9") + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "1.9" + assert state.attributes.get("latest_version") == "1.9" + assert state.attributes.get("release_summary") == "Test release summary" + assert state.attributes.get("release_url") == "https://example.com/release" + assert state.attributes.get("title") == "Test Update Title" + assert state.attributes.get("entity_picture") == "https://example.com/icon.png" + + async_fire_mqtt_message(hass, latest_version_topic, "2.0") + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_ON + assert state.attributes.get("installed_version") == "1.9" + assert state.attributes.get("latest_version") == "2.0" + + async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload with a template.""" installed_version_topic = "test/installed-version" @@ -155,6 +204,52 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("latest_version") == "2.0.0" +async def test_value_template_float(hass, mqtt_mock_entry_with_yaml_config): + """Test that it fetches the given payload with a template when the version is parsable as a number.""" + installed_version_topic = "test/installed-version" + latest_version_topic = "test/latest-version" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": installed_version_topic, + "value_template": "{{ value_json.installed }}", + "latest_version_topic": latest_version_topic, + "latest_version_template": "{{ value_json.latest }}", + "name": "Test Update", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, installed_version_topic, '{"installed":"1.9"}') + async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"1.9"}') + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "1.9" + assert state.attributes.get("latest_version") == "1.9" + assert ( + state.attributes.get("entity_picture") + == "https://brands.home-assistant.io/_/mqtt/icon.png" + ) + + async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"2.0"}') + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_ON + assert state.attributes.get("installed_version") == "1.9" + assert state.attributes.get("latest_version") == "2.0" + + async def test_empty_json_state_message(hass, mqtt_mock_entry_with_yaml_config): """Test an empty JSON payload.""" state_topic = "test/state-topic" @@ -527,3 +622,12 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): + """Test reloading the MQTT platform.""" + domain = update.DOMAIN + config = DEFAULT_CONFIG + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index b17a2bed457..562b8c95fd0 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -5,7 +5,15 @@ from unittest.mock import patch from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS import homeassistant.components.sensor as sensor -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_PLATFORM, CONF_TIMEOUT +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_NAME, + CONF_PLATFORM, + CONF_TIMEOUT, + CONF_UNIQUE_ID, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -82,3 +90,32 @@ async def test_room_update(hass, mqtt_mock_entry_with_yaml_config): await send_message(hass, BEDROOM_TOPIC, FAR_MESSAGE) await assert_state(hass, BEDROOM) await assert_distance(hass, 10) + + +async def test_unique_id_is_set( + hass: HomeAssistant, mqtt_mock_entry_with_yaml_config +) -> None: + """Test the updating between rooms.""" + unique_name = "my_unique_name_0123456789" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + CONF_PLATFORM: "mqtt_room", + CONF_NAME: NAME, + CONF_DEVICE_ID: DEVICE_ID, + CONF_STATE_TOPIC: "room_presence", + CONF_QOS: DEFAULT_QOS, + CONF_TIMEOUT: 5, + CONF_UNIQUE_ID: unique_name, + } + }, + ) + await hass.async_block_till_done() + state = hass.states.get(SENSOR_STATE) + assert state.state is not None + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(SENSOR_STATE) + assert entry.unique_id == unique_name diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index eb723405076..0f5befcac09 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -14,6 +14,9 @@ nam_data = { "software_version": "NAMF-2020-36", "uptime": "456987", "sensordatavalues": [ + {"value_type": "PMS_P0", "value": "6.00"}, + {"value_type": "PMS_P1", "value": "10.00"}, + {"value_type": "PMS_P2", "value": "11.00"}, {"value_type": "SDS_P1", "value": "18.65"}, {"value_type": "SDS_P2", "value": "11.03"}, {"value_type": "SPS30_P0", "value": "31.23"}, diff --git a/tests/components/nam/fixtures/diagnostics_data.json b/tests/components/nam/fixtures/diagnostics_data.json index 1506adaa824..67ba64576fd 100644 --- a/tests/components/nam/fixtures/diagnostics_data.json +++ b/tests/components/nam/fixtures/diagnostics_data.json @@ -11,6 +11,11 @@ "heca_humidity": 50.0, "heca_temperature": 8.0, "mhz14a_carbon_dioxide": 865, + "pms_caqi": 19, + "pms_caqi_level": "very low", + "pms_p0": 6, + "pms_p1": 10, + "pms_p2": 11, "sds011_caqi": 19, "sds011_caqi_level": "very low", "sds011_p1": 19, diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index bee4c515cd0..dc9e9a76d76 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -228,6 +228,73 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_caqi_level") + assert state + assert state.state == "very low" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_pmsx003_caqi_level") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi_level" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_caqi") + assert state + assert state.state == "19" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_pmsx003_caqi") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_particulate_matter_10") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get( + "sensor.nettigo_air_monitor_pmsx003_particulate_matter_10" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p1" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_particulate_matter_2_5") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get( + "sensor.nettigo_air_monitor_pmsx003_particulate_matter_2_5" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p2" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_particulate_matter_1_0") + assert state + assert state.state == "6" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get( + "sensor.nettigo_air_monitor_pmsx003_particulate_matter_1_0" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p0" + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_10") assert state assert state.state == "19" @@ -238,18 +305,20 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi") + entry = registry.async_get( + "sensor.nettigo_air_monitor_sds011_particulate_matter_10" + ) assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi") assert state assert state.state == "19" assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi_level") + entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi") assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi_level" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi" state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi_level") assert state @@ -257,11 +326,9 @@ async def test_sensor(hass): assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - entry = registry.async_get( - "sensor.nettigo_air_monitor_sds011_particulate_matter_10" - ) + entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi_level") assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi_level" state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") assert state @@ -279,6 +346,25 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p2" + state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi") + assert state + assert state.state == "54" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi_level") + assert state + assert state.state == "medium" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi_level") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi_level" + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0") assert state assert state.state == "31" @@ -289,25 +375,6 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi") - assert state - assert state.state == "54" - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - - entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi_level") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi_level" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi_level") - assert state - assert state.state == "medium" - assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0" ) diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 507a786c3d1..6a0158674e3 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -210,43 +210,33 @@ class MockClient: async def panic(self, code): """Handle panic.""" - pass async def disarm(self, code): """Handle disarm.""" - pass async def arm_away(self, code): """Handle arm_away.""" - pass async def arm_home(self, code): """Handle arm_home.""" - pass async def aux(self, output_id, state): """Handle auxiliary control.""" - pass async def keepalive(self): """Handle keepalive.""" - pass async def update(self): """Handle update.""" - pass def on_zone_change(self): """Handle on_zone_change.""" - pass def on_state_change(self): """Handle on_state_change.""" - pass async def close(self): """Handle close.""" - pass @pytest.fixture diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 458685cde70..db89063553f 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -82,7 +82,7 @@ class FakeAuth(AbstractAuth): @pytest.fixture -def aiohttp_client(loop, aiohttp_client, socket_enabled): +def aiohttp_client(event_loop, aiohttp_client, socket_enabled): """Return aiohttp_client and allow opening sockets.""" return aiohttp_client diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index 7d88ba1d329..a998a08d0c4 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -53,7 +53,9 @@ async def test_auth(hass, aioclient_mock): # Prepare to capture credentials for Subscriber captured_creds = None - async def async_new_subscriber(creds, subscription_name, loop, async_callback): + async def async_new_subscriber( + creds, subscription_name, event_loop, async_callback + ): """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds @@ -112,7 +114,9 @@ async def test_auth_expired_token(hass, aioclient_mock): # Prepare to capture credentials for Subscriber captured_creds = None - async def async_new_subscriber(creds, subscription_name, loop, async_callback): + async def async_new_subscriber( + creds, subscription_name, event_loop, async_callback + ): """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index b2ef95b138e..45e9c97550d 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -654,7 +654,7 @@ async def test_pubsub_subscriber_config_entry_reauth( result = await oauth.async_reauth(config_entry) await oauth.async_oauth_web_flow(result) - # Entering an updated access token refreshs the config entry. + # Entering an updated access token refreshes the config entry. entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") assert entry.unique_id == PROJECT_ID diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 8c80db1fdff..24f0004835d 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -52,6 +52,8 @@ SETTINGS = Settings( google_safe_browsing=False, idn_homograph_attacks_protection=True, logs=True, + logs_location="ch", + logs_retention=720, safesearch=False, threat_intelligence_feeds=True, typosquatting_protection=True, diff --git a/tests/components/nextdns/fixtures/settings.json b/tests/components/nextdns/fixtures/settings.json index 9593ca6325e..0a88ccb31ab 100644 --- a/tests/components/nextdns/fixtures/settings.json +++ b/tests/components/nextdns/fixtures/settings.json @@ -4,6 +4,8 @@ "cname_flattening": true, "anonymized_ecs": true, "logs": true, + "logs_location": "ch", + "logs_retention": 720, "web3": true, "allow_affiliate": true, "block_disguised_trackers": true, diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index f7dc08c41bb..4a0751ea74b 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -1,11 +1,16 @@ """Test the Nibe Heat Pump config flow.""" -import errno -from socket import gaierror -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from nibe.coil import Coil from nibe.connection import Connection -from nibe.exceptions import CoilNotFoundException, CoilReadException, CoilWriteException +from nibe.exceptions import ( + AddressInUseException, + CoilNotFoundException, + CoilReadException, + CoilReadSendException, + CoilWriteException, +) +import pytest from pytest import fixture from homeassistant import config_entries @@ -13,7 +18,7 @@ from homeassistant.components.nibe_heatpump import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -MOCK_FLOW_USERDATA = { +MOCK_FLOW_NIBEGW_USERDATA = { "model": "F1155", "ip_address": "127.0.0.1", "listening_port": 9999, @@ -22,13 +27,33 @@ MOCK_FLOW_USERDATA = { } -@fixture(autouse=True, name="mock_connection") -async def fixture_mock_connection(): +MOCK_FLOW_MODBUS_USERDATA = { + "model": "S1155", + "modbus_url": "tcp://127.0.0.1", + "modbus_unit": 0, +} + + +@fixture(autouse=True, name="mock_connection_constructor") +async def fixture_mock_connection_constructor(): """Make sure we have a dummy connection.""" + mock_constructor = Mock() with patch( - "homeassistant.components.nibe_heatpump.config_flow.NibeGW", spec=Connection - ) as mock_connection: - yield mock_connection + "homeassistant.components.nibe_heatpump.config_flow.NibeGW", + new=mock_constructor, + ), patch( + "homeassistant.components.nibe_heatpump.config_flow.Modbus", + new=mock_constructor, + ): + yield mock_constructor + + +@fixture(name="mock_connection") +def fixture_mock_connection(mock_connection_constructor: Mock): + """Make sure we have a dummy connection.""" + mock_connection = AsyncMock(spec=Connection) + mock_connection_constructor.return_value = mock_connection + return mock_connection @fixture(autouse=True, name="mock_setup_entry") @@ -40,24 +65,38 @@ async def fixture_mock_setup(): yield mock_setup_entry -async def test_form( - hass: HomeAssistant, mock_connection: Mock, mock_setup_entry: Mock -) -> None: +async def _get_connection_form( + hass: HomeAssistant, connection_type: str +) -> FlowResultType: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": connection_type} + ) + assert result["type"] == FlowResultType.FORM assert result["errors"] is None + return result + + +async def test_nibegw_form( + hass: HomeAssistant, mock_connection: Mock, mock_setup_entry: Mock +) -> None: + """Test we get the form.""" + result = await _get_connection_form(hass, "nibegw") coil_wordswap = Coil( 48852, "modbus40-word-swap-48852", "Modbus40 Word Swap", "u8", min=0, max=1 ) coil_wordswap.value = "ON" - mock_connection.return_value.read_coil.return_value = coil_wordswap + mock_connection.read_coil.return_value = coil_wordswap result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA + result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) await hass.async_block_till_done() @@ -75,109 +114,175 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 -async def test_address_inuse(hass: HomeAssistant, mock_connection: Mock) -> None: - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) +async def test_modbus_form( + hass: HomeAssistant, mock_connection: Mock, mock_setup_entry: Mock +) -> None: + """Test we get the form.""" + result = await _get_connection_form(hass, "modbus") - error = OSError() - error.errno = errno.EADDRINUSE - mock_connection.return_value.start.side_effect = error + coil = Coil( + 40022, "reset-alarm-40022", "Reset Alarm", "u8", min=0, max=1, write=True + ) + coil.value = "ON" + mock_connection.read_coil.return_value = coil result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA + result["flow_id"], MOCK_FLOW_MODBUS_USERDATA + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "S1155 at 127.0.0.1" + assert result2["data"] == { + "model": "S1155", + "modbus_url": "tcp://127.0.0.1", + "modbus_unit": 0, + "connection_type": "modbus", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_modbus_invalid_url( + hass: HomeAssistant, mock_connection_constructor: Mock +) -> None: + """Test we handle invalid auth.""" + result = await _get_connection_form(hass, "modbus") + + mock_connection_constructor.side_effect = ValueError() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**MOCK_FLOW_MODBUS_USERDATA, "modbus_url": "invalid://url"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"modbus_url": "url"} + + +async def test_nibegw_address_inuse(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle invalid auth.""" + result = await _get_connection_form(hass, "nibegw") + + mock_connection.start.side_effect = AddressInUseException() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"listening_port": "address_in_use"} - error.errno = errno.EACCES - mock_connection.return_value.start.side_effect = error + mock_connection.start.side_effect = Exception() result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA + result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} -async def test_read_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_read_timeout( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = CoilReadException() + mock_connection.verify_connectivity.side_effect = CoilReadException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "read"} -async def test_write_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_write_timeout( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.write_coil.side_effect = CoilWriteException() + mock_connection.verify_connectivity.side_effect = CoilWriteException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "write"} -async def test_unexpected_exception(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_unexpected_exception( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = Exception() + mock_connection.verify_connectivity.side_effect = Exception() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} -async def test_invalid_host(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_nibegw_invalid_host( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = gaierror() + mock_connection.verify_connectivity.side_effect = CoilReadSendException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {**MOCK_FLOW_USERDATA, "ip_address": "abcd"} - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"ip_address": "address"} + if connection_type == "nibegw": + assert result2["errors"] == {"ip_address": "address"} + else: + assert result2["errors"] == {"modbus_url": "address"} -async def test_model_missing_coil(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_model_missing_coil( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = CoilNotFoundException() + mock_connection.verify_connectivity.side_effect = CoilNotFoundException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {**MOCK_FLOW_USERDATA} - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "model"} diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 133c3a15ccb..4a6e9551b0b 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -159,7 +159,7 @@ async def test_climate_thermostat_schedule_temporary_hold(hass): # opportunistic set state = hass.states.get("climate.temp_bathroom") assert state.attributes["preset_mode"] == "Temporary Hold" - assert state.attributes["temperature"] == 50.0 + assert state.attributes["temperature"] == 90.0 # and the api poll returns it to the mock async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 98b30616952..630506623de 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -14,6 +14,7 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, @@ -848,3 +849,20 @@ async def test_custom_unit_change( state = hass.states.get(entity0.entity_id) assert float(state.state) == pytest.approx(float(default_value)) assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit + + +def test_device_classes_aligned(): + """Make sure all sensor device classes are also available in NumberDeviceClass.""" + + non_numeric_device_classes = { + SensorDeviceClass.DATE, + SensorDeviceClass.DURATION, + SensorDeviceClass.TIMESTAMP, + } + + for device_class in SensorDeviceClass: + if device_class in non_numeric_device_classes: + continue + + assert hasattr(NumberDeviceClass, device_class.name) + assert getattr(NumberDeviceClass, device_class.name).value == device_class.value diff --git a/tests/components/octoprint/test_button.py b/tests/components/octoprint/test_button.py index 644c1e39437..cc99a8e4c1d 100644 --- a/tests/components/octoprint/test_button.py +++ b/tests/components/octoprint/test_button.py @@ -18,13 +18,13 @@ async def test_pause_job(hass: HomeAssistant): """Test the pause job button.""" await init_integration(hass, BUTTON_DOMAIN) - corrdinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN]["uuid"][ + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN]["uuid"][ "coordinator" ] # Test pausing the printer when it is printing with patch("pyoctoprintapi.OctoprintClient.pause_job") as pause_command: - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( {"state": {"flags": {"printing": True}}, "temperature": []} ) await hass.services.async_call( @@ -40,7 +40,7 @@ async def test_pause_job(hass: HomeAssistant): # Test pausing the printer when it is paused with patch("pyoctoprintapi.OctoprintClient.pause_job") as pause_command: - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( {"state": {"flags": {"printing": False, "paused": True}}, "temperature": []} ) await hass.services.async_call( @@ -58,7 +58,7 @@ async def test_pause_job(hass: HomeAssistant): with patch( "pyoctoprintapi.OctoprintClient.pause_job" ) as pause_command, pytest.raises(InvalidPrinterState): - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( { "state": {"flags": {"printing": False, "paused": False}}, "temperature": [], @@ -78,13 +78,13 @@ async def test_resume_job(hass: HomeAssistant): """Test the resume job button.""" await init_integration(hass, BUTTON_DOMAIN) - corrdinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN]["uuid"][ + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN]["uuid"][ "coordinator" ] # Test resuming the printer when it is paused with patch("pyoctoprintapi.OctoprintClient.resume_job") as resume_command: - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( {"state": {"flags": {"printing": False, "paused": True}}, "temperature": []} ) await hass.services.async_call( @@ -100,7 +100,7 @@ async def test_resume_job(hass: HomeAssistant): # Test resuming the printer when it is printing with patch("pyoctoprintapi.OctoprintClient.resume_job") as resume_command: - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( {"state": {"flags": {"printing": True, "paused": False}}, "temperature": []} ) await hass.services.async_call( @@ -118,7 +118,7 @@ async def test_resume_job(hass: HomeAssistant): with patch( "pyoctoprintapi.OctoprintClient.resume_job" ) as resume_command, pytest.raises(InvalidPrinterState): - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( { "state": {"flags": {"printing": False, "paused": False}}, "temperature": [], @@ -138,13 +138,13 @@ async def test_stop_job(hass: HomeAssistant): """Test the stop job button.""" await init_integration(hass, BUTTON_DOMAIN) - corrdinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN]["uuid"][ + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN]["uuid"][ "coordinator" ] # Test stopping the printer when it is paused with patch("pyoctoprintapi.OctoprintClient.cancel_job") as stop_command: - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( {"state": {"flags": {"printing": False, "paused": True}}, "temperature": []} ) await hass.services.async_call( @@ -160,7 +160,7 @@ async def test_stop_job(hass: HomeAssistant): # Test stopping the printer when it is printing with patch("pyoctoprintapi.OctoprintClient.cancel_job") as stop_command: - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( {"state": {"flags": {"printing": True, "paused": False}}, "temperature": []} ) await hass.services.async_call( @@ -176,7 +176,7 @@ async def test_stop_job(hass: HomeAssistant): # Test stopping the printer when it is stopped with patch("pyoctoprintapi.OctoprintClient.cancel_job") as stop_command: - corrdinator.data["printer"] = OctoprintPrinterInfo( + coordinator.data["printer"] = OctoprintPrinterInfo( { "state": {"flags": {"printing": False, "paused": False}}, "temperature": [], diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index 3caa41749ee..b2c0a6c7ec5 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -56,8 +56,6 @@ def data_uv_index_fixture(): async def setup_openuv_fixture(hass, config, data_protection_window, data_uv_index): """Define a fixture to set up OpenUV.""" with patch( - "homeassistant.components.openuv.async_get_entity_id_from_unique_id_suffix", - ), patch( "homeassistant.components.openuv.Client.uv_index", return_value=data_uv_index ), patch( "homeassistant.components.openuv.Client.uv_protection_window", diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 9f51728365b..555d01e2624 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -2,10 +2,11 @@ from unittest.mock import patch from pyopenuv.errors import InvalidApiKeyError +import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.openuv import CONF_FROM_WINDOW, CONF_TO_WINDOW, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_ELEVATION, @@ -36,6 +37,13 @@ async def test_invalid_api_key(hass, config): assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} +def _get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: + for k in data_schema.schema: + if k == key and isinstance(k, vol.Marker): + return k + return None + + async def test_options_flow(hass, config_entry): """Test config flow options.""" with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): @@ -43,6 +51,13 @@ async def test_options_flow(hass, config_entry): result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" + # Original schema uses defaults for suggested values + assert _get_schema_marker( + result["data_schema"], CONF_FROM_WINDOW + ).description == {"suggested_value": 3.5} + assert _get_schema_marker( + result["data_schema"], CONF_TO_WINDOW + ).description == {"suggested_value": 3.5} result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} @@ -50,6 +65,39 @@ async def test_options_flow(hass, config_entry): assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} + # Subsequent schema uses previous input for suggested values + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert _get_schema_marker( + result["data_schema"], CONF_FROM_WINDOW + ).description == {"suggested_value": 3.5} + assert _get_schema_marker( + result["data_schema"], CONF_TO_WINDOW + ).description == {"suggested_value": 2.0} + + +async def test_step_reauth(hass, config, config_entry, setup_openuv): + """Test that the reauth step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=config + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_API_KEY: "new_api_key"} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1 + async def test_step_user(hass, config, setup_openuv): """Test that the user step works.""" diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index 856d3ece298..f7f978f381e 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -35,7 +35,6 @@ LOCATION_MESSAGE = { @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): """Mock device tracker config loading.""" - pass @pytest.fixture diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index e0069cf9b75..dfda844c7a2 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -31,6 +31,10 @@ def mock_tv(): tv.notify_change_supported = False tv.pairing_type = None tv.powerstate = None + tv.source_id = None + tv.ambilight_current_configuration = None + tv.ambilight_styles = {} + tv.ambilight_cached = {} with patch( "homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv @@ -42,7 +46,7 @@ def mock_tv(): async def mock_config_entry(hass): """Get standard player.""" config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME, unique_id="ABCDEFGHIJKLF" + domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME, unique_id=MOCK_SERIAL_NO ) config_entry.add_to_hass(hass) return config_entry diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index dd06ee25d49..2c5b21b1e34 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -34,6 +34,7 @@ async def test_get_triggers(hass, mock_device): triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, mock_device.id ) + triggers = [trigger for trigger in triggers if trigger["domain"] == DOMAIN] assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/picnic/test_services.py b/tests/components/picnic/test_services.py new file mode 100644 index 00000000000..ab7ff859d6e --- /dev/null +++ b/tests/components/picnic/test_services.py @@ -0,0 +1,211 @@ +"""Tests for the Picnic services.""" +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.picnic import CONF_COUNTRY_CODE, DOMAIN +from homeassistant.components.picnic.const import SERVICE_ADD_PRODUCT_TO_CART +from homeassistant.components.picnic.services import PicnicServiceException +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +UNIQUE_ID = "295-6y3-1nf4" + + +def create_picnic_api_client(unique_id): + """Create PicnicAPI mock with set response data.""" + auth_token = "af3wh738j3fa28l9fa23lhiufahu7l" + auth_data = { + "user_id": unique_id, + "address": { + "street": "Teststreet", + "house_number": 123, + "house_number_ext": "b", + }, + } + picnic_mock = MagicMock() + picnic_mock.session.auth_token = auth_token + picnic_mock.get_user.return_value = auth_data + + return picnic_mock + + +async def create_picnic_config_entry(hass: HomeAssistant, unique_id): + """Create a Picnic config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ACCESS_TOKEN: "x-original-picnic-auth-token", + CONF_COUNTRY_CODE: "NL", + }, + unique_id=unique_id, + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture +def picnic_api_client(): + """Return the default picnic api client.""" + with patch( + "homeassistant.components.picnic.create_picnic_client" + ) as create_picnic_client_mock: + picnic_client_mock = create_picnic_api_client(UNIQUE_ID) + create_picnic_client_mock.return_value = picnic_client_mock + + yield picnic_client_mock + + +@pytest.fixture +async def picnic_config_entry(hass: HomeAssistant): + """Generate the default Picnic config entry.""" + return await create_picnic_config_entry(hass, UNIQUE_ID) + + +async def test_add_product_using_id( + hass: HomeAssistant, + picnic_api_client: MagicMock, + picnic_config_entry: MockConfigEntry, +): + """Test adding a product by id.""" + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, + { + "config_entry_id": picnic_config_entry.entry_id, + "product_id": "5109348572", + "amount": 3, + }, + blocking=True, + ) + + # Check that the right method is called on the api + picnic_api_client.add_product.assert_called_with("5109348572", 3) + + +async def test_add_product_using_name( + hass: HomeAssistant, + picnic_api_client: MagicMock, + picnic_config_entry: MockConfigEntry, +): + """Test adding a product by name.""" + + # Set the return value of the search api endpoint + picnic_api_client.search.return_value = [ + { + "items": [ + { + "id": "2525404", + "name": "Best tea", + "display_price": 321, + "unit_quantity": "big bags", + }, + { + "id": "2525500", + "name": "Cheap tea", + "display_price": 100, + "unit_quantity": "small bags", + }, + ] + } + ] + + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, + {"config_entry_id": picnic_config_entry.entry_id, "product_name": "Tea"}, + blocking=True, + ) + + # Check that the right method is called on the api + picnic_api_client.add_product.assert_called_with("2525404", 1) + + +async def test_add_product_using_name_no_results( + hass: HomeAssistant, + picnic_api_client: MagicMock, + picnic_config_entry: MockConfigEntry, +): + """Test adding a product by name that can't be found.""" + + # Set the search return value and check that the right exception is raised during the service call + picnic_api_client.search.return_value = [] + with pytest.raises(PicnicServiceException): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, + { + "config_entry_id": picnic_config_entry.entry_id, + "product_name": "Random non existing product", + }, + blocking=True, + ) + + +async def test_add_product_using_name_no_named_results( + hass: HomeAssistant, + picnic_api_client: MagicMock, + picnic_config_entry: MockConfigEntry, +): + """Test adding a product by name for which no named results are returned.""" + + # Set the search return value and check that the right exception is raised during the service call + picnic_api_client.search.return_value = [{"items": [{"attr": "test"}]}] + with pytest.raises(PicnicServiceException): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, + { + "config_entry_id": picnic_config_entry.entry_id, + "product_name": "Random product", + }, + blocking=True, + ) + + +async def test_add_product_multiple_config_entries( + hass: HomeAssistant, + picnic_api_client: MagicMock, + picnic_config_entry: MockConfigEntry, +): + """Test adding a product for a specific Picnic service while multiple are configured.""" + with patch( + "homeassistant.components.picnic.create_picnic_client" + ) as create_picnic_client_mock: + picnic_api_client_2 = create_picnic_api_client("3fj9-9gju-236") + create_picnic_client_mock.return_value = picnic_api_client_2 + picnic_config_entry_2 = await create_picnic_config_entry(hass, "3fj9-9gju-236") + + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, + {"product_id": "5109348572", "config_entry_id": picnic_config_entry_2.entry_id}, + blocking=True, + ) + + # Check that the right method is called on the api + picnic_api_client.add_product.assert_not_called() + picnic_api_client_2.add_product.assert_called_with("5109348572", 1) + + +async def test_add_product_device_doesnt_exist( + hass: HomeAssistant, + picnic_api_client: MagicMock, + picnic_config_entry: MockConfigEntry, +): + """Test adding a product for a specific Picnic service, which doesn't exist.""" + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRODUCT_TO_CART, + {"product_id": "5109348572", "config_entry_id": 12345}, + blocking=True, + ) + + # Check that the right method is called on the api + picnic_api_client.add_product.assert_not_called() diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index aa34fc8bed6..0c70a2bed6d 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -75,7 +75,7 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]: chosen_env = "adam_multiple_devices_per_zone" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value @@ -101,7 +101,7 @@ def mock_smile_adam_2() -> Generator[None, MagicMock, None]: chosen_env = "m_adam_heating" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value @@ -127,7 +127,7 @@ def mock_smile_adam_3() -> Generator[None, MagicMock, None]: chosen_env = "m_adam_cooling" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value @@ -152,7 +152,7 @@ def mock_smile_anna() -> Generator[None, MagicMock, None]: """Create a Mock Anna environment for testing exceptions.""" chosen_env = "anna_heatpump_heating" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value @@ -177,7 +177,7 @@ def mock_smile_anna_2() -> Generator[None, MagicMock, None]: """Create a 2nd Mock Anna environment for testing exceptions.""" chosen_env = "m_anna_heatpump_cooling" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value @@ -202,7 +202,7 @@ def mock_smile_anna_3() -> Generator[None, MagicMock, None]: """Create a 3rd Mock Anna environment for testing exceptions.""" chosen_env = "m_anna_heatpump_idle" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value @@ -227,7 +227,7 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]: """Create a Mock P1 DSMR environment for testing exceptions.""" chosen_env = "p1v3_full_option" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value @@ -252,7 +252,7 @@ def mock_stretch() -> Generator[None, MagicMock, None]: """Create a Mock Stretch environment for testing exceptions.""" chosen_env = "stretch_v31" with patch( - "homeassistant.components.plugwise.gateway.Smile", autospec=True + "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 546a11b2c68..f00293a6554 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -3,7 +3,7 @@ "smile_name": "Smile Anna", "gateway_id": "015ae9ea3f964e668e490fa39da3870b", "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", - "cooling_present": true, + "cooling_present": false, "notifications": {} }, { @@ -21,17 +21,18 @@ }, "available": true, "binary_sensors": { + "cooling_enabled": false, "dhw_state": false, "heating_state": true, "compressor_state": true, - "cooling_state": false, "slave_boiler_state": false, "flame_state": false }, "sensors": { "water_temperature": 29.1, + "domestic_hot_water_setpoint": 60.0, "dhw_temperature": 46.3, - "intended_boiler_temperature": 0.0, + "intended_boiler_temperature": 35.0, "modulation_level": 52, "return_temperature": 25.1, "water_pressure": 1.57, @@ -66,13 +67,11 @@ "name": "Anna", "vendor": "Plugwise", "thermostat": { - "setpoint_low": 20.5, - "setpoint_high": 24.0, + "setpoint": 20.5, "lower_bound": 4.0, "upper_bound": 30.0, "resolution": 0.1 }, - "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], @@ -81,11 +80,10 @@ "mode": "auto", "sensors": { "temperature": 19.3, + "setpoint": 20.5, "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, - "cooling_deactivation_threshold": 4.0, - "setpoint_low": 20.5, - "setpoint_high": 24.0 + "cooling_deactivation_threshold": 4.0 } } } diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index 6326a02fedb..ba980a7fce3 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -21,6 +21,7 @@ }, "available": true, "binary_sensors": { + "cooling_enabled": true, "dhw_state": false, "heating_state": false, "compressor_state": true, @@ -29,13 +30,14 @@ "flame_state": false }, "sensors": { - "water_temperature": 29.1, - "dhw_temperature": 46.3, + "water_temperature": 22.7, + "domestic_hot_water_setpoint": 60.0, + "dhw_temperature": 41.5, "intended_boiler_temperature": 0.0, - "modulation_level": 52, - "return_temperature": 25.1, + "modulation_level": 40, + "return_temperature": 23.8, "water_pressure": 1.57, - "outdoor_air_temperature": 3.0 + "outdoor_air_temperature": 28.0 }, "switches": { "dhw_cm_switch": false @@ -72,7 +74,6 @@ "upper_bound": 30.0, "resolution": 0.1 }, - "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index cd2747f423b..0a421be5343 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -21,6 +21,7 @@ }, "available": true, "binary_sensors": { + "cooling_enabled": true, "dhw_state": false, "heating_state": false, "compressor_state": false, @@ -72,7 +73,6 @@ "upper_bound": 30.0, "resolution": 0.1 }, - "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 1e4df8fb673..f4f2a3f3c5f 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -26,7 +26,7 @@ async def test_anna_climate_binary_sensor_entities( assert state assert state.state == STATE_ON - state = hass.states.get("binary_sensor.opentherm_cooling") + state = hass.states.get("binary_sensor.opentherm_cooling_enabled") assert state assert state.state == STATE_OFF diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index ad5443a678c..5636523a919 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -202,7 +202,7 @@ async def test_anna_climate_entity_attributes( assert state.state == HVACMode.AUTO assert state.attributes["hvac_action"] == "heating" assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT_COOL, + HVACMode.HEAT, HVACMode.AUTO, ] @@ -211,9 +211,8 @@ async def test_anna_climate_entity_attributes( assert state.attributes["current_temperature"] == 19.3 assert state.attributes["preset_mode"] == "home" - assert state.attributes["supported_features"] == 18 - assert state.attributes["target_temp_high"] == 24.0 - assert state.attributes["target_temp_low"] == 20.5 + assert state.attributes["supported_features"] == 17 + assert state.attributes["temperature"] == 20.5 assert state.attributes["min_temp"] == 4.0 assert state.attributes["max_temp"] == 30.0 assert state.attributes["target_temp_step"] == 0.1 @@ -286,7 +285,7 @@ async def test_anna_climate_entity_climate_changes( await hass.services.async_call( "climate", "set_hvac_mode", - {"entity_id": "climate.anna", "hvac_mode": "heat_cool"}, + {"entity_id": "climate.anna", "hvac_mode": "heat"}, blocking=True, ) diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 84d2335b16f..b569cb08ddf 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -5,7 +5,9 @@ from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, InvalidSetupError, - PlugwiseException, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, ) import pytest @@ -97,9 +99,12 @@ def mock_smile(): with patch( "homeassistant.components.plugwise.config_flow.Smile", ) as smile_mock: - smile_mock.PlugwiseException = PlugwiseException - smile_mock.InvalidAuthentication = InvalidAuthentication smile_mock.ConnectionFailedError = ConnectionFailedError + smile_mock.InvalidAuthentication = InvalidAuthentication + smile_mock.InvalidSetupError = InvalidSetupError + smile_mock.InvalidXMLError = InvalidXMLError + smile_mock.ResponseError = ResponseError + smile_mock.UnsupportedDeviceError = UnsupportedDeviceError smile_mock.return_value.connect.return_value = True yield smile_mock.return_value @@ -266,13 +271,15 @@ async def test_zercoconf_discovery_update_configuration( @pytest.mark.parametrize( - "side_effect,reason", + "side_effect, reason", [ + (ConnectionFailedError, "cannot_connect"), (InvalidAuthentication, "invalid_auth"), (InvalidSetupError, "invalid_setup"), - (ConnectionFailedError, "cannot_connect"), - (PlugwiseException, "cannot_connect"), + (InvalidXMLError, "response_error"), + (ResponseError, "response_error"), (RuntimeError, "unknown"), + (UnsupportedDeviceError, "unsupported"), ], ) async def test_flow_errors( diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 557680cb060..cbca5d19ab5 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -1,12 +1,12 @@ """Tests for the Plugwise Climate integration.""" -import asyncio from unittest.mock import MagicMock -import aiohttp from plugwise.exceptions import ( ConnectionFailedError, - PlugwiseException, - XMLDataMissingError, + InvalidAuthentication, + InvalidXMLError, + ResponseError, + UnsupportedDeviceError, ) import pytest @@ -44,30 +44,31 @@ async def test_load_unload_config_entry( @pytest.mark.parametrize( - "side_effect", + "side_effect, entry_state", [ - (ConnectionFailedError), - (PlugwiseException), - (XMLDataMissingError), - (asyncio.TimeoutError), - (aiohttp.ClientConnectionError), + (ConnectionFailedError, ConfigEntryState.SETUP_RETRY), + (InvalidAuthentication, ConfigEntryState.SETUP_ERROR), + (InvalidXMLError, ConfigEntryState.SETUP_RETRY), + (ResponseError, ConfigEntryState.SETUP_RETRY), + (UnsupportedDeviceError, ConfigEntryState.SETUP_ERROR), ], ) -async def test_config_entry_not_ready( +async def test_gateway_config_entry_not_ready( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_smile_anna: MagicMock, side_effect: Exception, + entry_state: ConfigEntryState, ) -> None: """Test the Plugwise configuration entry not ready.""" - mock_smile_anna.connect.side_effect = side_effect + mock_smile_anna.async_update.side_effect = side_effect mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert len(mock_smile_anna.connect.mock_calls) == 1 - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is entry_state @pytest.mark.parametrize( diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 9039c5a476e..b08c2113d80 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -47,6 +47,10 @@ async def test_anna_as_smt_climate_sensor_entities( assert state assert float(state.state) == 29.1 + state = hass.states.get("sensor.opentherm_dhw_setpoint") + assert state + assert float(state.state) == 60.0 + state = hass.states.get("sensor.opentherm_dhw_temperature") assert state assert float(state.state) == 46.3 diff --git a/tests/components/pushbullet/__init__.py b/tests/components/pushbullet/__init__.py index c7f7911950c..74c89f33f2b 100644 --- a/tests/components/pushbullet/__init__.py +++ b/tests/components/pushbullet/__init__.py @@ -1 +1,5 @@ """Tests for the pushbullet component.""" + +from homeassistant.const import CONF_API_KEY, CONF_NAME + +MOCK_CONFIG = {CONF_NAME: "pushbullet", CONF_API_KEY: "MYAPIKEY"} diff --git a/tests/components/pushbullet/conftest.py b/tests/components/pushbullet/conftest.py new file mode 100644 index 00000000000..5fadff3a825 --- /dev/null +++ b/tests/components/pushbullet/conftest.py @@ -0,0 +1,28 @@ +"""Conftest for pushbullet integration.""" + +from pushbullet import PushBullet +import pytest +from requests_mock import Mocker + +from tests.common import load_fixture + + +@pytest.fixture(autouse=True) +def requests_mock_fixture(requests_mock: Mocker) -> None: + """Fixture to provide a aioclient mocker.""" + requests_mock.get( + PushBullet.DEVICES_URL, + text=load_fixture("devices.json", "pushbullet"), + ) + requests_mock.get( + PushBullet.ME_URL, + text=load_fixture("user_info.json", "pushbullet"), + ) + requests_mock.get( + PushBullet.CHATS_URL, + text=load_fixture("chats.json", "pushbullet"), + ) + requests_mock.get( + PushBullet.CHANNELS_URL, + text=load_fixture("channels.json", "pushbullet"), + ) diff --git a/tests/components/pushbullet/fixtures/channels.json b/tests/components/pushbullet/fixtures/channels.json new file mode 100644 index 00000000000..b95ca50b7c2 --- /dev/null +++ b/tests/components/pushbullet/fixtures/channels.json @@ -0,0 +1,14 @@ +{ + "channels": [ + { + "active": true, + "created": 1412047948.579029, + "description": "Sample channel.", + "iden": "ujxPklLhvyKsjAvkMyTVh6", + "image_url": "https://dl.pushbulletusercontent.com/abc123/image.jpg", + "modified": 1412047948.579031, + "name": "Sample channel", + "tag": "sample-channel" + } + ] +} diff --git a/tests/components/pushbullet/fixtures/chats.json b/tests/components/pushbullet/fixtures/chats.json new file mode 100644 index 00000000000..4c52bcc58cc --- /dev/null +++ b/tests/components/pushbullet/fixtures/chats.json @@ -0,0 +1,18 @@ +{ + "chats": [ + { + "active": true, + "created": 1412047948.579029, + "iden": "ujpah72o0sjAoRtnM0jc", + "modified": 1412047948.579031, + "with": { + "email": "someone@example.com", + "email_normalized": "someone@example.com", + "iden": "ujlMns72k", + "image_url": "https://dl.pushbulletusercontent.com/acb123/example.jpg", + "name": "Someone", + "type": "user" + } + } + ] +} diff --git a/tests/components/pushbullet/fixtures/user_info.json b/tests/components/pushbullet/fixtures/user_info.json new file mode 100644 index 00000000000..3a17cccbf07 --- /dev/null +++ b/tests/components/pushbullet/fixtures/user_info.json @@ -0,0 +1,10 @@ +{ + "created": 1381092887.398433, + "email": "example@email.com", + "email_normalized": "example@email.com", + "iden": "ujpah72o0", + "image_url": "https://static.pushbullet.com/missing-image/55a7dc-45", + "max_upload_size": 26214400, + "modified": 1441054560.741007, + "name": "Some name" +} diff --git a/tests/components/pushbullet/test_config_flow.py b/tests/components/pushbullet/test_config_flow.py new file mode 100644 index 00000000000..a19c424c8be --- /dev/null +++ b/tests/components/pushbullet/test_config_flow.py @@ -0,0 +1,134 @@ +"""Test pushbullet config flow.""" +from unittest.mock import patch + +from pushbullet import InvalidKeyError, PushbulletError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.pushbullet.const import DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def pushbullet_setup_fixture(): + """Patch pushbullet setup entry.""" + with patch( + "homeassistant.components.pushbullet.async_setup_entry", return_value=True + ): + yield + + +async def test_flow_user(hass: HomeAssistant, requests_mock_fixture) -> None: + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "pushbullet" + assert result["data"] == MOCK_CONFIG + + +async def test_flow_user_already_configured( + hass: HomeAssistant, requests_mock_fixture +) -> None: + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="ujpah72o0", + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_name_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="MYAPIKEY", + ) + + entry.add_to_hass(hass) + + new_config = MOCK_CONFIG.copy() + new_config[CONF_API_KEY] = "NEWKEY" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=new_config, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_invalid_key(hass: HomeAssistant) -> None: + """Test user initialized flow with invalid api key.""" + + with patch( + "homeassistant.components.pushbullet.config_flow.PushBullet", + side_effect=InvalidKeyError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_flow_conn_error(hass: HomeAssistant) -> None: + """Test user initialized flow with conn error.""" + + with patch( + "homeassistant.components.pushbullet.config_flow.PushBullet", + side_effect=PushbulletError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_import(hass: HomeAssistant, requests_mock_fixture) -> None: + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "pushbullet" + assert result["data"] == MOCK_CONFIG diff --git a/tests/components/pushbullet/test_init.py b/tests/components/pushbullet/test_init.py new file mode 100644 index 00000000000..6f8e3776b35 --- /dev/null +++ b/tests/components/pushbullet/test_init.py @@ -0,0 +1,84 @@ +"""Test pushbullet integration.""" +from unittest.mock import patch + +from pushbullet import InvalidKeyError, PushbulletError + +from homeassistant.components.pushbullet.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import HomeAssistant + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +async def test_async_setup_entry_success( + hass: HomeAssistant, requests_mock_fixture +) -> None: + """Test pushbullet successful setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.pushbullet.api.PushBulletNotificationProvider.start" + ) as mock_start: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + mock_start.assert_called_once() + + +async def test_setup_entry_failed_invalid_key(hass: HomeAssistant) -> None: + """Test pushbullet failed setup due to invalid key.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.pushbullet.PushBullet", + side_effect=InvalidKeyError, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_setup_entry_failed_conn_error(hass: HomeAssistant) -> None: + """Test pushbullet failed setup due to conn error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.pushbullet.PushBullet", + side_effect=PushbulletError, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_async_unload_entry(hass: HomeAssistant, requests_mock_fixture) -> None: + """Test pushbullet unload entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index a9186652f64..a9c93e88bdb 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -1,109 +1,65 @@ -"""The tests for the pushbullet notification platform.""" +"""Test pushbullet notification platform.""" from http import HTTPStatus -import json -from unittest.mock import patch -from pushbullet import PushBullet -import pytest +from requests_mock import Mocker -import homeassistant.components.notify as notify -from homeassistant.setup import async_setup_component +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.pushbullet.const import DOMAIN -from tests.common import assert_setup_component, load_fixture +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry -@pytest.fixture -def mock_pushbullet(): - """Mock pushbullet.""" - with patch.object( - PushBullet, - "_get_data", - return_value=json.loads(load_fixture("devices.json", "pushbullet")), - ): - yield - - -async def test_pushbullet_config(hass, mock_pushbullet): - """Test setup.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] - - -async def test_pushbullet_config_bad(hass): - """Test set up the platform with bad/missing configuration.""" - config = {notify.DOMAIN: {"platform": "pushbullet"}} - with assert_setup_component(0) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert not handle_config[notify.DOMAIN] - - -async def test_pushbullet_push_default(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_default(hass, requests_mock: Mocker): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) - data = {"title": "Test Title", "message": "Test Message"} - await hass.services.async_call(notify.DOMAIN, "test", data) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = {"title": "Test Title", "message": "Test Message"} + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 1 expected_body = {"body": "Test Message", "title": "Test Title", "type": "note"} + assert requests_mock.last_request assert requests_mock.last_request.json() == expected_body -async def test_pushbullet_push_device(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_device(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = { "title": "Test Title", "message": "Test Message", "target": ["device/DESKTOP"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 1 expected_body = { "body": "Test Message", @@ -114,35 +70,29 @@ async def test_pushbullet_push_device(hass, requests_mock, mock_pushbullet): assert requests_mock.last_request.json() == expected_body -async def test_pushbullet_push_devices(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_devices(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = { "title": "Test Title", "message": "Test Message", "target": ["device/DESKTOP", "device/My iPhone"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 2 - assert len(requests_mock.request_history) == 2 expected_body = { "body": "Test Message", @@ -150,45 +100,39 @@ async def test_pushbullet_push_devices(hass, requests_mock, mock_pushbullet): "title": "Test Title", "type": "note", } - assert requests_mock.request_history[0].json() == expected_body + assert requests_mock.request_history[-2].json() == expected_body expected_body = { "body": "Test Message", "device_iden": "identity2", "title": "Test Title", "type": "note", } - assert requests_mock.request_history[1].json() == expected_body + assert requests_mock.request_history[-1].json() == expected_body -async def test_pushbullet_push_email(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_email(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = { "title": "Test Title", "message": "Test Message", "target": ["email/user@host.net"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 1 - assert len(requests_mock.request_history) == 1 expected_body = { "body": "Test Message", @@ -196,38 +140,33 @@ async def test_pushbullet_push_email(hass, requests_mock, mock_pushbullet): "title": "Test Title", "type": "note", } - assert requests_mock.request_history[0].json() == expected_body + assert requests_mock.last_request.json() == expected_body -async def test_pushbullet_push_mixed(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_mixed(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = { "title": "Test Title", "message": "Test Message", "target": ["device/DESKTOP", "email/user@host.net"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 2 - assert len(requests_mock.request_history) == 2 expected_body = { "body": "Test Message", @@ -235,40 +174,11 @@ async def test_pushbullet_push_mixed(hass, requests_mock, mock_pushbullet): "title": "Test Title", "type": "note", } - assert requests_mock.request_history[0].json() == expected_body + assert requests_mock.request_history[-2].json() == expected_body expected_body = { "body": "Test Message", "email": "user@host.net", "title": "Test Title", "type": "note", } - assert requests_mock.request_history[1].json() == expected_body - - -async def test_pushbullet_push_no_file(hass, requests_mock, mock_pushbullet): - """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] - requests_mock.register_uri( - "POST", - "https://api.pushbullet.com/v2/pushes", - status_code=HTTPStatus.OK, - json={"mock_response": "Ok"}, - ) - data = { - "title": "Test Title", - "message": "Test Message", - "target": ["device/DESKTOP", "device/My iPhone"], - "data": {"file": "not_a_file"}, - } - assert not await hass.services.async_call(notify.DOMAIN, "test", data) - await hass.async_block_till_done() + assert requests_mock.request_history[-1].json() == expected_body diff --git a/tests/components/qnap_qsw/test_binary_sensor.py b/tests/components/qnap_qsw/test_binary_sensor.py index a36a34f02be..a270e78f051 100644 --- a/tests/components/qnap_qsw/test_binary_sensor.py +++ b/tests/components/qnap_qsw/test_binary_sensor.py @@ -1,17 +1,81 @@ """The binary sensor tests for the QNAP QSW platform.""" +from unittest.mock import AsyncMock + from homeassistant.components.qnap_qsw.const import ATTR_MESSAGE -from homeassistant.const import STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from .util import async_init_integration -async def test_qnap_qsw_create_binary_sensors(hass: HomeAssistant) -> None: +async def test_qnap_qsw_create_binary_sensors( + hass: HomeAssistant, entity_registry_enabled_by_default: AsyncMock +) -> None: """Test creation of binary sensors.""" await async_init_integration(hass) + er = entity_registry.async_get(hass) state = hass.states.get("binary_sensor.qsw_m408_4c_anomaly") assert state.state == STATE_OFF assert state.attributes.get(ATTR_MESSAGE) is None + + state = hass.states.get("binary_sensor.qsw_m408_4c_lacp_port_1_link") + assert state.state == STATE_OFF + entry = er.async_get(state.entity_id) + assert entry.unique_id == "qsw_unique_id_ports-status_lacp_port_1_link" + + state = hass.states.get("binary_sensor.qsw_m408_4c_lacp_port_2_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_lacp_port_3_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_lacp_port_4_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_lacp_port_5_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_lacp_port_6_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_1_link") + assert state.state == STATE_ON + entry = er.async_get(state.entity_id) + assert entry.unique_id == "qsw_unique_id_ports-status_port_1_link" + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_2_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_3_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_4_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_5_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_6_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_7_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_8_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_9_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_10_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_11_link") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.qsw_m408_4c_port_12_link") + assert state.state == STATE_OFF diff --git a/tests/components/qnap_qsw/test_sensor.py b/tests/components/qnap_qsw/test_sensor.py index 3caf223c808..b37a45e441e 100644 --- a/tests/components/qnap_qsw/test_sensor.py +++ b/tests/components/qnap_qsw/test_sensor.py @@ -1,12 +1,17 @@ """The sensor tests for the QNAP QSW platform.""" +from unittest.mock import AsyncMock + from homeassistant.components.qnap_qsw.const import ATTR_MAX from homeassistant.core import HomeAssistant from .util import async_init_integration -async def test_qnap_qsw_create_sensors(hass: HomeAssistant) -> None: +async def test_qnap_qsw_create_sensors( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, +) -> None: """Test creation of sensors.""" await async_init_integration(hass) @@ -17,9 +22,28 @@ async def test_qnap_qsw_create_sensors(hass: HomeAssistant) -> None: state = hass.states.get("sensor.qsw_m408_4c_fan_2_speed") assert state is None + state = hass.states.get("sensor.qsw_m408_4c_ports") + assert state.state == "3" + assert state.attributes.get(ATTR_MAX) == 12 + + state = hass.states.get("sensor.qsw_m408_4c_rx_errors") + assert state.state == "22" + + state = hass.states.get("sensor.qsw_m408_4c_rx") + assert state.state == "22200" + + state = hass.states.get("sensor.qsw_m408_4c_rx_speed") + assert state.state == "0" + state = hass.states.get("sensor.qsw_m408_4c_temperature") assert state.state == "31" assert state.attributes.get(ATTR_MAX) == 85 + state = hass.states.get("sensor.qsw_m408_4c_tx") + assert state.state == "11100" + + state = hass.states.get("sensor.qsw_m408_4c_tx_speed") + assert state.state == "0" + state = hass.states.get("sensor.qsw_m408_4c_uptime") assert state.state == "91" diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index 639d548e4be..7e574b1e3e0 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -7,10 +7,6 @@ from aiohttp.client_exceptions import ClientError from homeassistant.components.radarr.const import DOMAIN from homeassistant.const import ( CONF_API_KEY, - CONF_HOST, - CONF_MONITORED_CONDITIONS, - CONF_PORT, - CONF_SSL, CONF_URL, CONF_VERIFY_SSL, CONTENT_TYPE_JSON, @@ -32,15 +28,6 @@ MOCK_USER_INPUT = { CONF_VERIFY_SSL: False, } -CONF_IMPORT_DATA = { - CONF_API_KEY: API_KEY, - CONF_HOST: "192.168.1.189", - CONF_MONITORED_CONDITIONS: ["Stream count"], - CONF_PORT: "7887", - "urlbase": "/test", - CONF_SSL: False, -} - CONF_DATA = { CONF_URL: URL, CONF_API_KEY: API_KEY, diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py index 6aac4e369fe..0e328b50f94 100644 --- a/tests/components/radarr/test_config_flow.py +++ b/tests/components/radarr/test_config_flow.py @@ -1,18 +1,17 @@ """Test Radarr config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from aiopyarr import exceptions from homeassistant import data_entry_flow from homeassistant.components.radarr.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from . import ( API_KEY, CONF_DATA, - CONF_IMPORT_DATA, MOCK_REAUTH_INPUT, MOCK_USER_INPUT, URL, @@ -23,52 +22,9 @@ from . import ( setup_integration, ) -from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker -def _patch_setup(): - return patch("homeassistant.components.radarr.async_setup_entry") - - -async def test_flow_import(hass: HomeAssistant): - """Test import step.""" - with patch( - "homeassistant.components.radarr.config_flow.RadarrClient.async_get_system_status", - return_value=AsyncMock(), - ), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"] == CONF_DATA | { - CONF_URL: "http://192.168.1.189:7887/test" - } - assert result["data"][CONF_URL] == "http://192.168.1.189:7887/test" - - -async def test_flow_import_already_configured(hass: HomeAssistant): - """Test import step already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.radarr.config_flow.RadarrClient.async_get_system_status", - return_value=AsyncMock(), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index deb03d65cb5..5f5b769b6dd 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -6,7 +6,11 @@ from regenmaschine.errors import RainMachineError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import zeroconf -from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN +from homeassistant.components.rainmachine import ( + CONF_DEFAULT_ZONE_RUN_TIME, + CONF_USE_APP_RUN_TIMES, + DOMAIN, +) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.helpers import entity_registry as er @@ -99,10 +103,14 @@ async def test_options_flow(hass, config, config_entry): assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_ZONE_RUN_TIME: 600} + result["flow_id"], + user_input={CONF_DEFAULT_ZONE_RUN_TIME: 600, CONF_USE_APP_RUN_TIMES: False}, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_ZONE_RUN_TIME: 600} + assert config_entry.options == { + CONF_DEFAULT_ZONE_RUN_TIME: 600, + CONF_USE_APP_RUN_TIMES: False, + } async def test_show_form(hass): @@ -130,7 +138,7 @@ async def test_step_user(hass, config, setup_rainmachine): CONF_PASSWORD: "password", CONF_PORT: 8080, CONF_SSL: True, - CONF_ZONE_RUN_TIME: 600, + CONF_DEFAULT_ZONE_RUN_TIME: 600, } @@ -238,7 +246,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( CONF_PASSWORD: "password", CONF_PORT: 8080, CONF_SSL: True, - CONF_ZONE_RUN_TIME: 600, + CONF_DEFAULT_ZONE_RUN_TIME: 600, } diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index a3c03c956a4..084818eeef1 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -20,7 +20,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach "port": 8080, "ssl": True, }, - "options": {}, + "options": {"use_app_run_times": False}, "pref_disable_new_entities": False, "pref_disable_polling": False, "source": "user", @@ -642,7 +642,7 @@ async def test_entry_diagnostics_failed_controller_diagnostics( "port": 8080, "ssl": True, }, - "options": {}, + "options": {"use_app_run_times": False}, "pref_disable_new_entities": False, "pref_disable_polling": False, "source": "user", diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index c36fcbd1642..0f672ef98db 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "1", "revision": None, }, + "config_entries": [config_entry.entry_id], "dongle": None, "name": "Raspberry Pi", "url": None, diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 0ddc76e4423..ce9ed30797b 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -5,7 +5,7 @@ import asyncio from dataclasses import dataclass from datetime import datetime import time -from typing import Any, cast +from typing import Any, Literal, cast from sqlalchemy import create_engine from sqlalchemy.orm.session import Session @@ -53,7 +53,7 @@ def do_adhoc_statistics(hass: HomeAssistant, **kwargs: Any) -> None: """Trigger an adhoc statistics run.""" if not (start := kwargs.get("start")): start = statistics.get_start_time() - get_instance(hass).queue_task(StatisticsTask(start)) + get_instance(hass).queue_task(StatisticsTask(start, False)) def wait_recording_done(hass: HomeAssistant) -> None: @@ -137,3 +137,21 @@ def run_information_with_session( session.expunge(res) return cast(RecorderRuns, res) return res + + +def statistics_during_period( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None = None, + statistic_ids: list[str] | None = None, + period: Literal["5minute", "day", "hour", "week", "month"] = "hour", + units: dict[str, str] | None = None, + types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]] + | None = None, +) -> dict[str, list[dict[str, Any]]]: + """Call statistics_during_period with defaults for simpler tests.""" + if types is None: + types = {"last_reset", "max", "mean", "min", "state", "sum"} + return statistics.statistics_during_period( + hass, start_time, end_time, statistic_ids, period, units, types + ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index ca4cbc9a4f9..e13f1b873bd 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -26,8 +26,13 @@ from homeassistant.components.recorder import ( Recorder, get_instance, pool, + statistics, +) +from homeassistant.components.recorder.const import ( + EVENT_RECORDER_5MIN_STATISTICS_GENERATED, + EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, + KEEPALIVE_TIME, ) -from homeassistant.components.recorder.const import KEEPALIVE_TIME from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, EventData, @@ -933,7 +938,7 @@ def test_auto_purge_disabled(hass_recorder): @pytest.mark.parametrize("enable_statistics", [True]) -def test_auto_statistics(hass_recorder): +def test_auto_statistics(hass_recorder, freezer): """Test periodic statistics scheduling.""" hass = hass_recorder() @@ -942,43 +947,82 @@ def test_auto_statistics(hass_recorder): tz = dt_util.get_time_zone("Europe/Copenhagen") dt_util.set_default_time_zone(tz) + stats_5min = [] + stats_hourly = [] + + @callback + def async_5min_stats_updated_listener(event: Event) -> None: + """Handle recorder 5 min stat updated.""" + stats_5min.append(event) + + def async_hourly_stats_updated_listener(event: Event) -> None: + """Handle recorder 5 min stat updated.""" + stats_hourly.append(event) + # Statistics is scheduled to happen every 5 minutes. Exercise this behavior by # firing time changed events and advancing the clock around this time. Pick an # arbitrary year in the future to avoid boundary conditions relative to the current # date. # - # The clock is started at 4:16am then advanced forward below + # The clock is started at 4:51am then advanced forward below now = dt_util.utcnow() - test_time = datetime(now.year + 2, 1, 1, 4, 16, 0, tzinfo=tz) + test_time = datetime(now.year + 2, 1, 1, 4, 51, 0, tzinfo=tz) + freezer.move_to(test_time.isoformat()) run_tasks_at_time(hass, test_time) + hass.block_till_done() + hass.bus.listen( + EVENT_RECORDER_5MIN_STATISTICS_GENERATED, async_5min_stats_updated_listener + ) + hass.bus.listen( + EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, async_hourly_stats_updated_listener + ) + + real_compile_statistics = statistics.compile_statistics with patch( "homeassistant.components.recorder.statistics.compile_statistics", - return_value=True, + side_effect=real_compile_statistics, + autospec=True, ) as compile_statistics: # Advance 5 minutes, and the statistics task should run test_time = test_time + timedelta(minutes=5) + freezer.move_to(test_time.isoformat()) run_tasks_at_time(hass, test_time) assert len(compile_statistics.mock_calls) == 1 + hass.block_till_done() + assert len(stats_5min) == 1 + assert len(stats_hourly) == 0 compile_statistics.reset_mock() # Advance 5 minutes, and the statistics task should run again test_time = test_time + timedelta(minutes=5) + freezer.move_to(test_time.isoformat()) run_tasks_at_time(hass, test_time) assert len(compile_statistics.mock_calls) == 1 + hass.block_till_done() + assert len(stats_5min) == 2 + assert len(stats_hourly) == 1 compile_statistics.reset_mock() # Advance less than 5 minutes. The task should not run. test_time = test_time + timedelta(minutes=3) + freezer.move_to(test_time.isoformat()) run_tasks_at_time(hass, test_time) assert len(compile_statistics.mock_calls) == 0 + hass.block_till_done() + assert len(stats_5min) == 2 + assert len(stats_hourly) == 1 # Advance 5 minutes, and the statistics task should run again test_time = test_time + timedelta(minutes=5) + freezer.move_to(test_time.isoformat()) run_tasks_at_time(hass, test_time) assert len(compile_statistics.mock_calls) == 1 + hass.block_till_done() + assert len(stats_5min) == 3 + assert len(stats_hourly) == 1 dt_util.set_default_time_zone(original_tz) @@ -1027,8 +1071,27 @@ def test_compile_missing_statistics(tmpdir, freezer): hass.stop() # Start Home Assistant one hour later + stats_5min = [] + stats_hourly = [] + + @callback + def async_5min_stats_updated_listener(event: Event) -> None: + """Handle recorder 5 min stat updated.""" + stats_5min.append(event) + + def async_hourly_stats_updated_listener(event: Event) -> None: + """Handle recorder 5 min stat updated.""" + stats_hourly.append(event) + freezer.tick(timedelta(hours=1)) hass = get_test_home_assistant() + hass.bus.listen( + EVENT_RECORDER_5MIN_STATISTICS_GENERATED, async_5min_stats_updated_listener + ) + hass.bus.listen( + EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, async_hourly_stats_updated_listener + ) + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() @@ -1041,6 +1104,9 @@ def test_compile_missing_statistics(tmpdir, freezer): last_run = process_timestamp(statistics_runs[1].start) assert last_run == now + assert len(stats_5min) == 1 + assert len(stats_hourly) == 1 + wait_recording_done(hass) wait_recording_done(hass) hass.stop() diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index aae6fcf91cb..d3a1c8b7fe0 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,21 +1,24 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name -from datetime import timedelta +from datetime import datetime, timedelta import importlib import sys -from unittest.mock import patch, sentinel +from unittest.mock import ANY, DEFAULT, MagicMock, patch, sentinel import pytest from pytest import approx from sqlalchemy import create_engine +from sqlalchemy.exc import OperationalError from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import history, statistics from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import StatisticsShortTerm -from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.models import process_timestamp from homeassistant.components.recorder.statistics import ( + _statistics_during_period_with_session, + _update_or_add_metadata, async_add_external_statistics, async_import_statistics, delete_statistics_duplicates, @@ -25,7 +28,6 @@ from homeassistant.components.recorder.statistics import ( get_latest_short_term_statistics, get_metadata, list_statistic_ids, - statistics_during_period, ) from homeassistant.components.recorder.util import session_scope from homeassistant.const import TEMP_CELSIUS @@ -35,7 +37,12 @@ from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done, do_adhoc_statistics, wait_recording_done +from .common import ( + async_wait_recording_done, + do_adhoc_statistics, + statistics_during_period, + wait_recording_done, +) from tests.common import get_test_home_assistant, mock_registry @@ -52,22 +59,29 @@ def test_compile_hourly_statistics(hass_recorder): assert dict(states) == dict(hist) # Should not fail if there is nothing there yet - stats = get_latest_short_term_statistics(hass, ["sensor.test1"]) + stats = get_latest_short_term_statistics( + hass, ["sensor.test1"], {"last_reset", "max", "mean", "min", "state", "sum"} + ) assert stats == {} for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics( + hass, + 0, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {} do_adhoc_statistics(hass, start=zero) do_adhoc_statistics(hass, start=four) wait_recording_done(hass) expected_1 = { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), @@ -76,9 +90,8 @@ def test_compile_hourly_statistics(hass_recorder): "sum": None, } expected_2 = { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(four), - "end": process_timestamp_to_utc_isoformat(four + timedelta(minutes=5)), + "start": process_timestamp(four), + "end": process_timestamp(four + timedelta(minutes=5)), "mean": approx(20.0), "min": approx(20.0), "max": approx(20.0), @@ -86,14 +99,8 @@ def test_compile_hourly_statistics(hass_recorder): "state": None, "sum": None, } - expected_stats1 = [ - {**expected_1, "statistic_id": "sensor.test1"}, - {**expected_2, "statistic_id": "sensor.test1"}, - ] - expected_stats2 = [ - {**expected_1, "statistic_id": "sensor.test2"}, - {**expected_2, "statistic_id": "sensor.test2"}, - ] + expected_stats1 = [expected_1, expected_2] + expected_stats2 = [expected_1, expected_2] # Test statistics_during_period stats = statistics_during_period(hass, zero, period="5minute") @@ -119,32 +126,71 @@ def test_compile_hourly_statistics(hass_recorder): assert stats == {} # Test get_last_short_term_statistics and get_latest_short_term_statistics - stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics( + hass, + 0, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {} - stats = get_last_short_term_statistics(hass, 1, "sensor.test1", True) - assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} + stats = get_last_short_term_statistics( + hass, + 1, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) + assert stats == {"sensor.test1": [expected_2]} - stats = get_latest_short_term_statistics(hass, ["sensor.test1"]) - assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} + stats = get_latest_short_term_statistics( + hass, ["sensor.test1"], {"last_reset", "max", "mean", "min", "state", "sum"} + ) + assert stats == {"sensor.test1": [expected_2]} metadata = get_metadata(hass, statistic_ids=['sensor.test1"']) - stats = get_latest_short_term_statistics(hass, ["sensor.test1"], metadata=metadata) - assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} + stats = get_latest_short_term_statistics( + hass, + ["sensor.test1"], + {"last_reset", "max", "mean", "min", "state", "sum"}, + metadata=metadata, + ) + assert stats == {"sensor.test1": [expected_2]} - stats = get_last_short_term_statistics(hass, 2, "sensor.test1", True) + stats = get_last_short_term_statistics( + hass, + 2, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_short_term_statistics(hass, 3, "sensor.test1", True) + stats = get_last_short_term_statistics( + hass, + 3, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_short_term_statistics(hass, 1, "sensor.test3", True) + stats = get_last_short_term_statistics( + hass, + 1, + "sensor.test3", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {} instance.get_session().query(StatisticsShortTerm).delete() # Should not fail there is nothing in the table - stats = get_latest_short_term_statistics(hass, ["sensor.test1"]) + stats = get_latest_short_term_statistics( + hass, ["sensor.test1"], {"last_reset", "max", "mean", "min", "state", "sum"} + ) assert stats == {} @@ -218,9 +264,8 @@ def test_compile_periodic_statistics_exception( do_adhoc_statistics(hass, start=now + timedelta(minutes=5)) wait_recording_done(hass) expected_1 = { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(now), - "end": process_timestamp_to_utc_isoformat(now + timedelta(minutes=5)), + "start": process_timestamp(now), + "end": process_timestamp(now + timedelta(minutes=5)), "mean": None, "min": None, "max": None, @@ -229,9 +274,8 @@ def test_compile_periodic_statistics_exception( "sum": None, } expected_2 = { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(now + timedelta(minutes=5)), - "end": process_timestamp_to_utc_isoformat(now + timedelta(minutes=10)), + "start": process_timestamp(now + timedelta(minutes=5)), + "end": process_timestamp(now + timedelta(minutes=10)), "mean": None, "min": None, "max": None, @@ -239,17 +283,9 @@ def test_compile_periodic_statistics_exception( "state": None, "sum": None, } - expected_stats1 = [ - {**expected_1, "statistic_id": "sensor.test1"}, - {**expected_2, "statistic_id": "sensor.test1"}, - ] - expected_stats2 = [ - {**expected_2, "statistic_id": "sensor.test2"}, - ] - expected_stats3 = [ - {**expected_1, "statistic_id": "sensor.test3"}, - {**expected_2, "statistic_id": "sensor.test3"}, - ] + expected_stats1 = [expected_1, expected_2] + expected_stats2 = [expected_2] + expected_stats3 = [expected_1, expected_2] stats = statistics_during_period(hass, now, period="5minute") assert stats == { @@ -286,15 +322,20 @@ def test_rename_entity(hass_recorder): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics( + hass, + 0, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {} do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) expected_1 = { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), @@ -302,15 +343,9 @@ def test_rename_entity(hass_recorder): "state": None, "sum": None, } - expected_stats1 = [ - {**expected_1, "statistic_id": "sensor.test1"}, - ] - expected_stats2 = [ - {**expected_1, "statistic_id": "sensor.test2"}, - ] - expected_stats99 = [ - {**expected_1, "statistic_id": "sensor.test99"}, - ] + expected_stats1 = [expected_1] + expected_stats2 = [expected_1] + expected_stats99 = [expected_1] stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} @@ -353,15 +388,20 @@ def test_rename_entity_collision(hass_recorder, caplog): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics( + hass, + 0, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {} do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) expected_1 = { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), @@ -369,12 +409,8 @@ def test_rename_entity_collision(hass_recorder, caplog): "state": None, "sum": None, } - expected_stats1 = [ - {**expected_1, "statistic_id": "sensor.test1"}, - ] - expected_stats2 = [ - {**expected_1, "statistic_id": "sensor.test2"}, - ] + expected_stats1 = [expected_1] + expected_stats2 = [expected_1] stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} @@ -465,7 +501,7 @@ async def test_import_statistics( zero = dt_util.utcnow() last_reset = dt_util.parse_datetime(last_reset_str) if last_reset_str else None - last_reset_utc_str = dt_util.as_utc(last_reset).isoformat() if last_reset else None + last_reset_utc = dt_util.as_utc(last_reset) if last_reset else None period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) @@ -497,24 +533,22 @@ async def test_import_statistics( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period1), + "end": process_timestamp(period1 + timedelta(hours=1)), "max": None, "mean": None, "min": None, - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(0.0), "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period2), + "end": process_timestamp(period2 + timedelta(hours=1)), "max": None, "mean": None, "min": None, - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(1.0), "sum": approx(3.0), }, @@ -523,6 +557,7 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -546,17 +581,22 @@ async def test_import_statistics( }, ) } - last_stats = get_last_statistics(hass, 1, statistic_id, True) + last_stats = get_last_statistics( + hass, + 1, + statistic_id, + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert last_stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period2), + "end": process_timestamp(period2 + timedelta(hours=1)), "max": None, "mean": None, "min": None, - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(1.0), "sum": approx(3.0), }, @@ -576,9 +616,8 @@ async def test_import_statistics( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period1), + "end": process_timestamp(period1 + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -587,13 +626,12 @@ async def test_import_statistics( "sum": approx(6.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period2), + "end": process_timestamp(period2 + timedelta(hours=1)), "max": None, "mean": None, "min": None, - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(1.0), "sum": approx(3.0), }, @@ -616,6 +654,7 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -643,24 +682,22 @@ async def test_import_statistics( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period1), + "end": process_timestamp(period1 + timedelta(hours=1)), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period2), + "end": process_timestamp(period2 + timedelta(hours=1)), "max": None, "mean": None, "min": None, - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(1.0), "sum": approx(3.0), }, @@ -686,24 +723,22 @@ async def test_import_statistics( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period1), + "end": process_timestamp(period1 + timedelta(hours=1)), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": process_timestamp(period2), + "end": process_timestamp(period2 + timedelta(hours=1)), "max": None, "mean": None, "min": None, - "last_reset": last_reset_utc_str, + "last_reset": last_reset_utc, "state": approx(1.0), "sum": approx(1000 * 1000 + 3.0), }, @@ -947,9 +982,8 @@ def test_weekly_statistics(hass_recorder, caplog, timezone): assert stats == { "test:total_energy_import": [ { - "statistic_id": "test:total_energy_import", - "start": week1_start.isoformat(), - "end": week1_end.isoformat(), + "start": week1_start, + "end": week1_end, "max": None, "mean": None, "min": None, @@ -958,9 +992,8 @@ def test_weekly_statistics(hass_recorder, caplog, timezone): "sum": 3.0, }, { - "statistic_id": "test:total_energy_import", - "start": week2_start.isoformat(), - "end": week2_end.isoformat(), + "start": week2_start, + "end": week2_end, "max": None, "mean": None, "min": None, @@ -980,9 +1013,8 @@ def test_weekly_statistics(hass_recorder, caplog, timezone): assert stats == { "test:total_energy_import": [ { - "statistic_id": "test:total_energy_import", - "start": week1_start.isoformat(), - "end": week1_end.isoformat(), + "start": week1_start, + "end": week1_end, "max": None, "mean": None, "min": None, @@ -991,9 +1023,8 @@ def test_weekly_statistics(hass_recorder, caplog, timezone): "sum": 3.0, }, { - "statistic_id": "test:total_energy_import", - "start": week2_start.isoformat(), - "end": week2_end.isoformat(), + "start": week2_start, + "end": week2_end, "max": None, "mean": None, "min": None, @@ -1085,9 +1116,8 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): assert stats == { "test:total_energy_import": [ { - "statistic_id": "test:total_energy_import", - "start": sep_start.isoformat(), - "end": sep_end.isoformat(), + "start": sep_start, + "end": sep_end, "max": None, "mean": None, "min": None, @@ -1096,9 +1126,8 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): "sum": approx(3.0), }, { - "statistic_id": "test:total_energy_import", - "start": oct_start.isoformat(), - "end": oct_end.isoformat(), + "start": oct_start, + "end": oct_end, "max": None, "mean": None, "min": None, @@ -1122,9 +1151,8 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): assert stats == { "test:total_energy_import": [ { - "statistic_id": "test:total_energy_import", - "start": sep_start.isoformat(), - "end": sep_end.isoformat(), + "start": sep_start, + "end": sep_end, "max": None, "mean": None, "min": None, @@ -1133,9 +1161,8 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): "sum": approx(3.0), }, { - "statistic_id": "test:total_energy_import", - "start": oct_start.isoformat(), - "end": oct_end.isoformat(), + "start": oct_start, + "end": oct_end, "max": None, "mean": None, "min": None, @@ -1451,6 +1478,208 @@ def test_delete_metadata_duplicates_no_duplicates(hass_recorder, caplog): assert "duplicated statistics_meta rows" not in caplog.text +@pytest.mark.parametrize("enable_statistics_table_validation", [True]) +@pytest.mark.parametrize("db_engine", ("mysql", "postgresql")) +async def test_validate_db_schema( + async_setup_recorder_instance, hass, caplog, db_engine +): + """Test validating DB schema with MySQL and PostgreSQL. + + Note: The test uses SQLite, the purpose is only to exercise the code. + """ + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine + ): + await async_setup_recorder_instance(hass) + await async_wait_recording_done(hass) + assert "Schema validation failed" not in caplog.text + assert "Detected statistics schema errors" not in caplog.text + assert "Database is about to correct DB schema errors" not in caplog.text + + +@pytest.mark.parametrize("enable_statistics_table_validation", [True]) +async def test_validate_db_schema_fix_utf8_issue( + async_setup_recorder_instance, hass, caplog +): + """Test validating DB schema with MySQL. + + Note: The test uses SQLite, the purpose is only to exercise the code. + """ + orig_error = MagicMock() + orig_error.args = [1366] + utf8_error = OperationalError("", "", orig=orig_error) + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" + ), patch( + "homeassistant.components.recorder.statistics._update_or_add_metadata", + side_effect=[utf8_error, DEFAULT, DEFAULT], + wraps=_update_or_add_metadata, + ): + await async_setup_recorder_instance(hass) + await async_wait_recording_done(hass) + + assert "Schema validation failed" not in caplog.text + assert ( + "Database is about to correct DB schema errors: statistics_meta.4-byte UTF-8" + in caplog.text + ) + assert ( + "Updating character set and collation of table statistics_meta to utf8mb4" + in caplog.text + ) + + +@pytest.mark.parametrize("enable_statistics_table_validation", [True]) +@pytest.mark.parametrize("db_engine", ("mysql", "postgresql")) +@pytest.mark.parametrize( + "table, replace_index", (("statistics", 0), ("statistics_short_term", 1)) +) +@pytest.mark.parametrize( + "column, value", + (("max", 1.0), ("mean", 1.0), ("min", 1.0), ("state", 1.0), ("sum", 1.0)), +) +async def test_validate_db_schema_fix_float_issue( + async_setup_recorder_instance, + hass, + caplog, + db_engine, + table, + replace_index, + column, + value, +): + """Test validating DB schema with MySQL. + + Note: The test uses SQLite, the purpose is only to exercise the code. + """ + orig_error = MagicMock() + orig_error.args = [1366] + precise_number = 1.000000000000001 + precise_time = datetime(2020, 10, 6, microsecond=1, tzinfo=dt_util.UTC) + statistics = { + "recorder.db_test": [ + { + "last_reset": precise_time, + "max": precise_number, + "mean": precise_number, + "min": precise_number, + "start": precise_time, + "state": precise_number, + "sum": precise_number, + } + ] + } + statistics["recorder.db_test"][0][column] = value + fake_statistics = [DEFAULT, DEFAULT] + fake_statistics[replace_index] = statistics + + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine + ), patch( + "homeassistant.components.recorder.statistics._statistics_during_period_with_session", + side_effect=fake_statistics, + wraps=_statistics_during_period_with_session, + ), patch( + "homeassistant.components.recorder.migration._modify_columns" + ) as modify_columns_mock: + await async_setup_recorder_instance(hass) + await async_wait_recording_done(hass) + + assert "Schema validation failed" not in caplog.text + assert ( + f"Database is about to correct DB schema errors: {table}.double precision" + in caplog.text + ) + modification = [ + "mean DOUBLE PRECISION", + "min DOUBLE PRECISION", + "max DOUBLE PRECISION", + "state DOUBLE PRECISION", + "sum DOUBLE PRECISION", + ] + modify_columns_mock.assert_called_once_with(ANY, ANY, table, modification) + + +@pytest.mark.parametrize("enable_statistics_table_validation", [True]) +@pytest.mark.parametrize( + "db_engine, modification", + ( + ("mysql", ["last_reset DATETIME(6)", "start DATETIME(6)"]), + ( + "postgresql", + [ + "last_reset TIMESTAMP(6) WITH TIME ZONE", + "start TIMESTAMP(6) WITH TIME ZONE", + ], + ), + ), +) +@pytest.mark.parametrize( + "table, replace_index", (("statistics", 0), ("statistics_short_term", 1)) +) +@pytest.mark.parametrize( + "column, value", + ( + ("last_reset", "2020-10-06T00:00:00+00:00"), + ("start", "2020-10-06T00:00:00+00:00"), + ), +) +async def test_validate_db_schema_fix_statistics_datetime_issue( + async_setup_recorder_instance, + hass, + caplog, + db_engine, + modification, + table, + replace_index, + column, + value, +): + """Test validating DB schema with MySQL. + + Note: The test uses SQLite, the purpose is only to exercise the code. + """ + orig_error = MagicMock() + orig_error.args = [1366] + precise_number = 1.000000000000001 + precise_time = datetime(2020, 10, 6, microsecond=1, tzinfo=dt_util.UTC) + statistics = { + "recorder.db_test": [ + { + "last_reset": precise_time, + "max": precise_number, + "mean": precise_number, + "min": precise_number, + "start": precise_time, + "state": precise_number, + "sum": precise_number, + } + ] + } + statistics["recorder.db_test"][0][column] = value + fake_statistics = [DEFAULT, DEFAULT] + fake_statistics[replace_index] = statistics + + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine + ), patch( + "homeassistant.components.recorder.statistics._statistics_during_period_with_session", + side_effect=fake_statistics, + wraps=_statistics_during_period_with_session, + ), patch( + "homeassistant.components.recorder.migration._modify_columns" + ) as modify_columns_mock: + await async_setup_recorder_instance(hass) + await async_wait_recording_done(hass) + + assert "Schema validation failed" not in caplog.text + assert ( + f"Database is about to correct DB schema errors: {table}.µs precision" + in caplog.text + ) + modify_columns_mock.assert_called_once_with(ANY, ANY, table, modification) + + def record_states(hass): """Record some test states. diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 9000379c17d..ecdd729a163 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1,9 +1,10 @@ """Test util methods.""" -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import os import sqlite3 from unittest.mock import MagicMock, Mock, patch +from freezegun import freeze_time import pytest from sqlalchemy import text from sqlalchemy.engine.result import ChunkedIteratorResult @@ -19,6 +20,7 @@ from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( end_incomplete_runs, is_second_sunday, + resolve_period, session_scope, ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -108,7 +110,7 @@ def test_validate_or_move_away_sqlite_database(hass, tmpdir, caplog): async def test_last_run_was_recently_clean( - loop, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path + event_loop, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path ): """Test we can check if the last recorder run was recently clean.""" config = { @@ -776,3 +778,80 @@ def test_execute_stmt_lambda_element(hass_recorder): with patch.object(session, "execute", MockExecutor): rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) assert rows == ["mock_row"] + + +@freeze_time(datetime(2022, 10, 21, 7, 25, tzinfo=timezone.utc)) +async def test_resolve_period(hass): + """Test statistic_during_period.""" + + now = dt_util.utcnow() + + start_t, end_t = resolve_period({"calendar": {"period": "hour"}}) + assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" + assert end_t.isoformat() == "2022-10-21T08:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "hour"}}) + assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" + assert end_t.isoformat() == "2022-10-21T08:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "hour", "offset": -1}}) + assert start_t.isoformat() == "2022-10-21T06:00:00+00:00" + assert end_t.isoformat() == "2022-10-21T07:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "day"}}) + assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" + assert end_t.isoformat() == "2022-10-22T07:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "day", "offset": -1}}) + assert start_t.isoformat() == "2022-10-20T07:00:00+00:00" + assert end_t.isoformat() == "2022-10-21T07:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "week"}}) + assert start_t.isoformat() == "2022-10-17T07:00:00+00:00" + assert end_t.isoformat() == "2022-10-24T07:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "week", "offset": -1}}) + assert start_t.isoformat() == "2022-10-10T07:00:00+00:00" + assert end_t.isoformat() == "2022-10-17T07:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "month"}}) + assert start_t.isoformat() == "2022-10-01T07:00:00+00:00" + assert end_t.isoformat() == "2022-11-01T07:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "month", "offset": -1}}) + assert start_t.isoformat() == "2022-09-01T07:00:00+00:00" + assert end_t.isoformat() == "2022-10-01T07:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "year"}}) + assert start_t.isoformat() == "2022-01-01T08:00:00+00:00" + assert end_t.isoformat() == "2023-01-01T08:00:00+00:00" + + start_t, end_t = resolve_period({"calendar": {"period": "year", "offset": -1}}) + assert start_t.isoformat() == "2021-01-01T08:00:00+00:00" + assert end_t.isoformat() == "2022-01-01T08:00:00+00:00" + + # Fixed period + assert resolve_period({}) == (None, None) + + assert resolve_period({"fixed_period": {"end_time": now}}) == (None, now) + + assert resolve_period({"fixed_period": {"start_time": now}}) == (now, None) + + assert resolve_period({"fixed_period": {"end_time": now, "start_time": now}}) == ( + now, + now, + ) + + # Rolling window + assert resolve_period( + {"rolling_window": {"duration": timedelta(hours=1, minutes=25)}} + ) == (now - timedelta(hours=1, minutes=25), now) + + assert resolve_period( + { + "rolling_window": { + "duration": timedelta(hours=1), + "offset": timedelta(minutes=-25), + } + } + ) == (now - timedelta(hours=1, minutes=25), now - timedelta(minutes=25)) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 423ab4bbc52..fefc8dbdda1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -17,7 +17,6 @@ from homeassistant.components.recorder.statistics import ( get_last_statistics, get_metadata, list_statistic_ids, - statistics_during_period, ) from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component @@ -29,6 +28,7 @@ from .common import ( async_wait_recording_done, create_engine_test, do_adhoc_statistics, + statistics_during_period, ) from tests.common import async_fire_time_changed @@ -167,9 +167,8 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client): assert response["result"] == { "sensor.test": [ { - "statistic_id": "sensor.test", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": approx(10), "min": approx(10), "max": approx(10), @@ -180,6 +179,28 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client): ] } + await client.send_json( + { + "id": 3, + "type": "recorder/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + "types": ["mean"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), + "mean": approx(10), + } + ] + } + @freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) @pytest.mark.parametrize("offset", (0, 1, 2)) @@ -895,9 +916,8 @@ async def test_statistics_during_period_unit_conversion( assert response["result"] == { "sensor.test": [ { - "statistic_id": "sensor.test", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": approx(value), "min": approx(value), "max": approx(value), @@ -924,9 +944,8 @@ async def test_statistics_during_period_unit_conversion( assert response["result"] == { "sensor.test": [ { - "statistic_id": "sensor.test", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": approx(converted_value), "min": approx(converted_value), "max": approx(converted_value), @@ -989,9 +1008,8 @@ async def test_sum_statistics_during_period_unit_conversion( assert response["result"] == { "sensor.test": [ { - "statistic_id": "sensor.test", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": None, "min": None, "max": None, @@ -1018,9 +1036,8 @@ async def test_sum_statistics_during_period_unit_conversion( assert response["result"] == { "sensor.test": [ { - "statistic_id": "sensor.test", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": None, "min": None, "max": None, @@ -1150,9 +1167,8 @@ async def test_statistics_during_period_in_the_past( assert response["result"] == { "sensor.test": [ { - "statistic_id": "sensor.test", - "start": stats_start.isoformat(), - "end": (stats_start + timedelta(minutes=5)).isoformat(), + "start": int(stats_start.timestamp() * 1000), + "end": int((stats_start + timedelta(minutes=5)).timestamp() * 1000), "mean": approx(10), "min": approx(10), "max": approx(10), @@ -1178,9 +1194,8 @@ async def test_statistics_during_period_in_the_past( assert response["result"] == { "sensor.test": [ { - "statistic_id": "sensor.test", - "start": start_of_day.isoformat(), - "end": (start_of_day + timedelta(days=1)).isoformat(), + "start": int(start_of_day.timestamp() * 1000), + "end": int((start_of_day + timedelta(days=1)).timestamp() * 1000), "mean": approx(10), "min": approx(10), "max": approx(10), @@ -1335,6 +1350,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1356,6 +1372,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1380,6 +1397,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1400,6 +1418,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1412,6 +1431,121 @@ async def test_list_statistic_ids( assert response["result"] == [] +@pytest.mark.parametrize( + "attributes, attributes2, display_unit, statistics_unit, unit_class", + [ + ( + DISTANCE_SENSOR_M_ATTRIBUTES, + DISTANCE_SENSOR_FT_ATTRIBUTES, + "ft", + "m", + "distance", + ), + ( + ENERGY_SENSOR_WH_ATTRIBUTES, + ENERGY_SENSOR_KWH_ATTRIBUTES, + "kWh", + "Wh", + "energy", + ), + (GAS_SENSOR_FT3_ATTRIBUTES, GAS_SENSOR_M3_ATTRIBUTES, "m³", "ft³", "volume"), + (POWER_SENSOR_KW_ATTRIBUTES, POWER_SENSOR_W_ATTRIBUTES, "W", "kW", "power"), + ( + PRESSURE_SENSOR_HPA_ATTRIBUTES, + PRESSURE_SENSOR_PA_ATTRIBUTES, + "Pa", + "hPa", + "pressure", + ), + ( + SPEED_SENSOR_KPH_ATTRIBUTES, + SPEED_SENSOR_MPH_ATTRIBUTES, + "mph", + "km/h", + "speed", + ), + ( + TEMPERATURE_SENSOR_C_ATTRIBUTES, + TEMPERATURE_SENSOR_F_ATTRIBUTES, + "°F", + "°C", + "temperature", + ), + ( + VOLUME_SENSOR_FT3_ATTRIBUTES, + VOLUME_SENSOR_M3_ATTRIBUTES, + "m³", + "ft³", + "volume", + ), + ], +) +async def test_list_statistic_ids_unit_change( + recorder_mock, + hass, + hass_ws_client, + attributes, + attributes2, + display_unit, + statistics_unit, + unit_class, +): + """Test list_statistic_ids.""" + now = dt_util.utcnow() + has_mean = attributes["state_class"] == "measurement" + has_sum = not has_mean + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + hass.states.async_set("sensor.test", 10, attributes=attributes) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + + await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "display_unit_of_measurement": statistics_unit, + "has_mean": has_mean, + "has_sum": has_sum, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, + } + ] + + # Change the state unit + hass.states.async_set("sensor.test", 10, attributes=attributes2) + + await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, + "has_mean": has_mean, + "has_sum": has_sum, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, + } + ] + + async def test_validate_statistics(recorder_mock, hass, hass_ws_client): """Test validate_statistics can be called.""" id = 1 @@ -1468,9 +1602,8 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): expected_response = { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": approx(value), "min": approx(value), "max": approx(value), @@ -1481,9 +1614,8 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): ], "sensor.test2": [ { - "statistic_id": "sensor.test2", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": approx(value * 2), "min": approx(value * 2), "max": approx(value * 2), @@ -1494,9 +1626,8 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): ], "sensor.test3": [ { - "statistic_id": "sensor.test3", - "start": now.isoformat(), - "end": (now + timedelta(minutes=5)).isoformat(), + "start": int(now.timestamp() * 1000), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "mean": approx(value * 3), "min": approx(value * 3), "max": approx(value * 3), @@ -1558,10 +1689,11 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): @pytest.mark.parametrize( - "new_unit, new_unit_class", [("dogs", None), (None, None), ("W", "power")] + "new_unit, new_unit_class, new_display_unit", + [("dogs", None, "dogs"), (None, None, None), ("W", "power", "kW")], ) async def test_update_statistics_metadata( - recorder_mock, hass, hass_ws_client, new_unit, new_unit_class + recorder_mock, hass, hass_ws_client, new_unit, new_unit_class, new_display_unit ): """Test removing statistics.""" now = dt_util.utcnow() @@ -1587,6 +1719,7 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1614,6 +1747,7 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": new_display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -1638,14 +1772,13 @@ async def test_update_statistics_metadata( assert response["result"] == { "sensor.test": [ { - "end": (now + timedelta(minutes=5)).isoformat(), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "last_reset": None, "max": 10.0, "mean": 10.0, "min": 10.0, - "start": now.isoformat(), + "start": int(now.timestamp() * 1000), "state": None, - "statistic_id": "sensor.test", "sum": None, } ], @@ -1677,6 +1810,7 @@ async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client): assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1700,14 +1834,13 @@ async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client): assert response["result"] == { "sensor.test": [ { - "end": (now + timedelta(minutes=5)).isoformat(), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "last_reset": None, "max": 10.0, "mean": 10.0, "min": 10.0, - "start": now.isoformat(), + "start": int(now.timestamp() * 1000), "state": None, - "statistic_id": "sensor.test", "sum": None, } ], @@ -1732,6 +1865,7 @@ async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client): assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1756,14 +1890,13 @@ async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client): assert response["result"] == { "sensor.test": [ { - "end": (now + timedelta(minutes=5)).isoformat(), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "last_reset": None, "max": 10000.0, "mean": 10000.0, "min": 10000.0, - "start": now.isoformat(), + "start": int(now.timestamp() * 1000), "state": None, - "statistic_id": "sensor.test", "sum": None, } ], @@ -1784,6 +1917,7 @@ async def test_change_statistics_unit_errors( expected_statistic_ids = [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1796,14 +1930,13 @@ async def test_change_statistics_unit_errors( expected_statistics = { "sensor.test": [ { - "end": (now + timedelta(minutes=5)).isoformat(), + "end": int((now + timedelta(minutes=5)).timestamp() * 1000), "last_reset": None, "max": 10.0, "mean": 10.0, "min": 10.0, - "start": now.isoformat(), + "start": int(now.timestamp() * 1000), "state": None, - "statistic_id": "sensor.test", "sum": None, } ], @@ -2169,6 +2302,7 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "test:total_gas", + "display_unit_of_measurement": unit, "has_mean": has_mean, "has_sum": has_sum, "name": "Total imported energy", @@ -2196,6 +2330,7 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -2223,6 +2358,7 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -2292,9 +2428,8 @@ async def test_import_statistics( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": (period1 + timedelta(hours=1)), "max": None, "mean": None, "min": None, @@ -2303,9 +2438,8 @@ async def test_import_statistics( "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2318,6 +2452,7 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -2341,13 +2476,18 @@ async def test_import_statistics( }, ) } - last_stats = get_last_statistics(hass, 1, statistic_id, True) + last_stats = get_last_statistics( + hass, + 1, + statistic_id, + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert last_stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2383,9 +2523,8 @@ async def test_import_statistics( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2394,9 +2533,8 @@ async def test_import_statistics( "sum": approx(6.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2435,9 +2573,8 @@ async def test_import_statistics( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), @@ -2446,9 +2583,8 @@ async def test_import_statistics( "sum": approx(5.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2519,9 +2655,8 @@ async def test_adjust_sum_statistics_energy( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2530,9 +2665,8 @@ async def test_adjust_sum_statistics_energy( "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2545,6 +2679,7 @@ async def test_adjust_sum_statistics_energy( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -2588,9 +2723,8 @@ async def test_adjust_sum_statistics_energy( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": approx(None), "mean": approx(None), "min": approx(None), @@ -2599,9 +2733,8 @@ async def test_adjust_sum_statistics_energy( "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2631,9 +2764,8 @@ async def test_adjust_sum_statistics_energy( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": approx(None), "mean": approx(None), "min": approx(None), @@ -2642,9 +2774,8 @@ async def test_adjust_sum_statistics_energy( "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2715,9 +2846,8 @@ async def test_adjust_sum_statistics_gas( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2726,9 +2856,8 @@ async def test_adjust_sum_statistics_gas( "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2741,6 +2870,7 @@ async def test_adjust_sum_statistics_gas( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { + "display_unit_of_measurement": "m³", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -2784,9 +2914,8 @@ async def test_adjust_sum_statistics_gas( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": approx(None), "mean": approx(None), "min": approx(None), @@ -2795,9 +2924,8 @@ async def test_adjust_sum_statistics_gas( "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2827,9 +2955,8 @@ async def test_adjust_sum_statistics_gas( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": approx(None), "mean": approx(None), "min": approx(None), @@ -2838,9 +2965,8 @@ async def test_adjust_sum_statistics_gas( "sum": approx(2.0), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2926,9 +3052,8 @@ async def test_adjust_sum_statistics_errors( assert stats == { statistic_id: [ { - "statistic_id": statistic_id, - "start": period1.isoformat(), - "end": (period1 + timedelta(hours=1)).isoformat(), + "start": period1, + "end": period1 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2937,9 +3062,8 @@ async def test_adjust_sum_statistics_errors( "sum": approx(2.0 * factor), }, { - "statistic_id": statistic_id, - "start": period2.isoformat(), - "end": (period2 + timedelta(hours=1)).isoformat(), + "start": period2, + "end": period2 + timedelta(hours=1), "max": None, "mean": None, "min": None, @@ -2953,6 +3077,7 @@ async def test_adjust_sum_statistics_errors( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": state_unit, "has_mean": False, "has_sum": True, "statistic_id": statistic_id, diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 785e27e1ea6..97499d19bea 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -27,7 +27,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_PASSWORD, CONF_USERNAME, - ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, @@ -398,12 +397,12 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state", }, { - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_ENTITY_ID: "sensor.reg_number_charging_power", + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_ENTITY_ID: "sensor.reg_number_admissible_charging_power", ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -617,12 +616,12 @@ MOCK_VEHICLES = { ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_state", }, { - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_ENTITY_ID: "sensor.reg_number_charging_power", + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_ENTITY_ID: "sensor.reg_number_admissible_charging_power", ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_power", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 9311b1cf024..513eb4071bc 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -6,8 +6,10 @@ from aiohttp import ClientWebSocketResponse from freezegun import freeze_time import pytest +from homeassistant.components.repairs import repairs_flow_manager from homeassistant.components.repairs.const import DOMAIN from homeassistant.components.repairs.issue_handler import ( + RepairsFlowManager, async_process_repairs_platforms, ) from homeassistant.const import __version__ as ha_version @@ -538,3 +540,14 @@ async def test_sync_methods( assert msg["success"] assert msg["result"] == {"issues": []} + + +async def test_flow_manager_helper(hass: HomeAssistant) -> None: + """Test accessing the repairs flow manager with the helper.""" + assert repairs_flow_manager(hass) is None + + assert await async_setup_component(hass, DOMAIN, {}) + + flow_manager = repairs_flow_manager(hass) + assert flow_manager is not None + assert isinstance(flow_manager, RepairsFlowManager) diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 0ba4c3043df..acbd7b879ab 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -1,9 +1,11 @@ """Test the repairs websocket API.""" from __future__ import annotations +from collections.abc import Awaitable, Callable from http import HTTPStatus from unittest.mock import ANY, AsyncMock, Mock +from aiohttp import ClientSession, ClientWebSocketResponse from freezegun import freeze_time import pytest import voluptuous as vol @@ -75,6 +77,7 @@ async def create_issues(hass, ws_client, issues=None): EXPECTED_DATA = { "issue_1": None, "issue_2": {"blah": "bleh"}, + "abort_issue1": None, } @@ -101,6 +104,16 @@ class MockFixFlow(RepairsFlow): return self.async_show_form(step_id="custom_step", data_schema=vol.Schema({})) +class MockFixFlowAbort(RepairsFlow): + """Handler for an issue fixing flow that aborts.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return self.async_abort(reason="not_given") + + @pytest.fixture(autouse=True) async def mock_repairs_integration(hass): """Mock a repairs integration.""" @@ -110,6 +123,8 @@ async def mock_repairs_integration(hass): assert issue_id in EXPECTED_DATA assert data == EXPECTED_DATA[issue_id] + if issue_id == "abort_issue1": + return MockFixFlowAbort() return MockFixFlow() mock_platform( @@ -491,3 +506,64 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> for issue in issues ] } + + +async def test_fix_issue_aborted( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test we can fix an issue.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues( + hass, + ws_client, + issues=[ + { + **DEFAULT_ISSUES[0], + "domain": "fake_integration", + "issue_id": "abort_issue1", + } + ], + ) + + await ws_client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + + first_issue = msg["result"]["issues"][0] + + assert first_issue["domain"] == "fake_integration" + assert first_issue["issue_id"] == "abort_issue1" + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": "fake_integration", "issue_id": "abort_issue1"}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "abort", + "flow_id": flow_id, + "handler": "fake_integration", + "reason": "not_given", + "description_placeholders": None, + "result": None, + } + + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert msg["result"]["issues"][0] == first_issue diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index a6655f6ddbc..757c331529e 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -5,6 +5,7 @@ from http import HTTPStatus from unittest.mock import MagicMock, patch import httpx +import pytest import respx from homeassistant import config as hass_config @@ -26,7 +27,7 @@ from homeassistant.setup import async_setup_component from tests.common import get_fixture_path -async def test_setup_missing_basic_config(hass): +async def test_setup_missing_basic_config(hass: HomeAssistant) -> None: """Test setup with configuration missing required entries.""" assert await async_setup_component( hass, Platform.BINARY_SENSOR, {"binary_sensor": {"platform": "rest"}} @@ -35,7 +36,7 @@ async def test_setup_missing_basic_config(hass): assert len(hass.states.async_all("binary_sensor")) == 0 -async def test_setup_missing_config(hass): +async def test_setup_missing_config(hass: HomeAssistant) -> None: """Test setup with configuration missing required entries.""" assert await async_setup_component( hass, @@ -53,7 +54,9 @@ async def test_setup_missing_config(hass): @respx.mock -async def test_setup_failed_connect(hass, caplog): +async def test_setup_failed_connect( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup when connection error occurs.""" respx.get("http://localhost").mock( @@ -76,7 +79,7 @@ async def test_setup_failed_connect(hass, caplog): @respx.mock -async def test_setup_timeout(hass): +async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs.""" respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( @@ -95,7 +98,7 @@ async def test_setup_timeout(hass): @respx.mock -async def test_setup_minimum(hass): +async def test_setup_minimum(hass: HomeAssistant) -> None: """Test setup with minimum configuration.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -114,7 +117,7 @@ async def test_setup_minimum(hass): @respx.mock -async def test_setup_minimum_resource_template(hass): +async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: """Test setup with minimum configuration (resource_template).""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -132,7 +135,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock -async def test_setup_duplicate_resource_template(hass): +async def test_setup_duplicate_resource_template(hass: HomeAssistant) -> None: """Test setup with duplicate resources.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -151,7 +154,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock -async def test_setup_get(hass): +async def test_setup_get(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -184,7 +187,7 @@ async def test_setup_get(hass): @respx.mock -async def test_setup_get_template_headers_params(hass): +async def test_setup_get_template_headers_params(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( @@ -218,7 +221,7 @@ async def test_setup_get_template_headers_params(hass): @respx.mock -async def test_setup_get_digest_auth(hass): +async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -246,7 +249,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock -async def test_setup_post(hass): +async def test_setup_post(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -274,7 +277,7 @@ async def test_setup_post(hass): @respx.mock -async def test_setup_get_off(hass): +async def test_setup_get_off(hass: HomeAssistant) -> None: """Test setup with valid off configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, @@ -304,7 +307,7 @@ async def test_setup_get_off(hass): @respx.mock -async def test_setup_get_on(hass): +async def test_setup_get_on(hass: HomeAssistant) -> None: """Test setup with valid on configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, @@ -334,7 +337,7 @@ async def test_setup_get_on(hass): @respx.mock -async def test_setup_with_exception(hass): +async def test_setup_with_exception(hass: HomeAssistant) -> None: """Test setup with exception.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -376,7 +379,7 @@ async def test_setup_with_exception(hass): @respx.mock -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload reset sensors.""" respx.get("http://localhost") % HTTPStatus.OK @@ -416,7 +419,7 @@ async def test_reload(hass): @respx.mock -async def test_setup_query_params(hass): +async def test_setup_query_params(hass: HomeAssistant) -> None: """Test setup with query params.""" respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 0c1670315fc..fbee3c2051a 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -27,7 +27,7 @@ from tests.common import ( @respx.mock -async def test_setup_with_endpoint_timeout_with_recovery(hass): +async def test_setup_with_endpoint_timeout_with_recovery(hass: HomeAssistant) -> None: """Test setup with an endpoint that times out that recovers.""" await async_setup_component(hass, "homeassistant", {}) @@ -134,7 +134,7 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass): @respx.mock -async def test_setup_minimum_resource_template(hass): +async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: """Test setup with minimum configuration (resource_template).""" respx.get("http://localhost").respond( @@ -192,7 +192,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload.""" respx.get("http://localhost") % HTTPStatus.OK @@ -241,7 +241,7 @@ async def test_reload(hass): @respx.mock -async def test_reload_and_remove_all(hass): +async def test_reload_and_remove_all(hass: HomeAssistant) -> None: """Verify we can reload and remove all.""" respx.get("http://localhost") % HTTPStatus.OK @@ -288,7 +288,7 @@ async def test_reload_and_remove_all(hass): @respx.mock -async def test_reload_fails_to_read_configuration(hass): +async def test_reload_fails_to_read_configuration(hass: HomeAssistant) -> None: """Verify reload when configuration is missing or broken.""" respx.get("http://localhost") % HTTPStatus.OK @@ -332,7 +332,7 @@ async def test_reload_fails_to_read_configuration(hass): @respx.mock -async def test_multiple_rest_endpoints(hass): +async def test_multiple_rest_endpoints(hass: HomeAssistant) -> None: """Test multiple rest endpoints.""" respx.get("http://date.jsontest.com").respond( diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index 31567ae63f0..f9a2e88c732 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -7,13 +7,14 @@ from homeassistant import config as hass_config import homeassistant.components.notify as notify from homeassistant.components.rest import DOMAIN from homeassistant.const import SERVICE_RELOAD +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @respx.mock -async def test_reload_notify(hass): +async def test_reload_notify(hass: HomeAssistant) -> None: """Verify we can reload the notify service.""" respx.get("http://localhost") % 200 diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index a89d20f2510..49ad69b1caa 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -4,6 +4,7 @@ from http import HTTPStatus from unittest.mock import MagicMock, patch import httpx +import pytest import respx from homeassistant import config as hass_config @@ -31,14 +32,14 @@ from homeassistant.setup import async_setup_component from tests.common import get_fixture_path -async def test_setup_missing_config(hass): +async def test_setup_missing_config(hass: HomeAssistant) -> None: """Test setup with configuration missing required entries.""" assert await async_setup_component(hass, DOMAIN, {"sensor": {"platform": "rest"}}) await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 0 -async def test_setup_missing_schema(hass): +async def test_setup_missing_schema(hass: HomeAssistant) -> None: """Test setup with resource missing schema.""" assert await async_setup_component( hass, @@ -50,7 +51,9 @@ async def test_setup_missing_schema(hass): @respx.mock -async def test_setup_failed_connect(hass, caplog): +async def test_setup_failed_connect( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup when connection error occurs.""" respx.get("http://localhost").mock( side_effect=httpx.RequestError("server offline", request=MagicMock()) @@ -72,7 +75,7 @@ async def test_setup_failed_connect(hass, caplog): @respx.mock -async def test_setup_timeout(hass): +async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs.""" respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( @@ -85,7 +88,7 @@ async def test_setup_timeout(hass): @respx.mock -async def test_setup_minimum(hass): +async def test_setup_minimum(hass: HomeAssistant) -> None: """Test setup with minimum configuration.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -104,7 +107,7 @@ async def test_setup_minimum(hass): @respx.mock -async def test_manual_update(hass): +async def test_manual_update(hass: HomeAssistant) -> None: """Test setup with minimum configuration.""" await async_setup_component(hass, "homeassistant", {}) respx.get("http://localhost").respond( @@ -140,7 +143,7 @@ async def test_manual_update(hass): @respx.mock -async def test_setup_minimum_resource_template(hass): +async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: """Test setup with minimum configuration (resource_template).""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -158,7 +161,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock -async def test_setup_duplicate_resource_template(hass): +async def test_setup_duplicate_resource_template(hass: HomeAssistant) -> None: """Test setup with duplicate resources.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -177,7 +180,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock -async def test_setup_get(hass): +async def test_setup_get(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -223,7 +226,9 @@ async def test_setup_get(hass): @respx.mock -async def test_setup_timestamp(hass, caplog): +async def test_setup_timestamp( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, json={"key": "2021-11-11 11:39Z"} @@ -286,7 +291,7 @@ async def test_setup_timestamp(hass, caplog): @respx.mock -async def test_setup_get_templated_headers_params(hass): +async def test_setup_get_templated_headers_params(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( @@ -320,7 +325,7 @@ async def test_setup_get_templated_headers_params(hass): @respx.mock -async def test_setup_get_digest_auth(hass): +async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -349,7 +354,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock -async def test_setup_post(hass): +async def test_setup_post(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -378,7 +383,7 @@ async def test_setup_post(hass): @respx.mock -async def test_setup_get_xml(hass): +async def test_setup_get_xml(hass: HomeAssistant) -> None: """Test setup with valid xml configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, @@ -410,7 +415,7 @@ async def test_setup_get_xml(hass): @respx.mock -async def test_setup_query_params(hass): +async def test_setup_query_params(hass: HomeAssistant) -> None: """Test setup with query params.""" respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( @@ -430,7 +435,7 @@ async def test_setup_query_params(hass): @respx.mock -async def test_update_with_json_attrs(hass): +async def test_update_with_json_attrs(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( @@ -463,7 +468,7 @@ async def test_update_with_json_attrs(hass): @respx.mock -async def test_update_with_no_template(hass): +async def test_update_with_no_template(hass: HomeAssistant) -> None: """Test update when there is no value template.""" respx.get("http://localhost").respond( @@ -495,7 +500,9 @@ async def test_update_with_no_template(hass): @respx.mock -async def test_update_with_json_attrs_no_data(hass, caplog): +async def test_update_with_json_attrs_no_data( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes when no JSON result fetched.""" respx.get("http://localhost").respond( @@ -531,7 +538,9 @@ async def test_update_with_json_attrs_no_data(hass, caplog): @respx.mock -async def test_update_with_json_attrs_not_dict(hass, caplog): +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( @@ -566,7 +575,9 @@ async def test_update_with_json_attrs_not_dict(hass, caplog): @respx.mock -async def test_update_with_json_attrs_bad_JSON(hass, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( @@ -602,7 +613,7 @@ async def test_update_with_json_attrs_bad_JSON(hass, caplog): @respx.mock -async def test_update_with_json_attrs_with_json_attrs_path(hass): +async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result with a template for the attributes.""" respx.get("http://localhost").respond( @@ -646,7 +657,9 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass): @respx.mock -async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): +async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( + hass: HomeAssistant, +) -> None: """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes.""" respx.get("http://localhost").respond( @@ -682,7 +695,9 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): @respx.mock -async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): +async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( + hass: HomeAssistant, +) -> None: """Test attributes get extracted from a JSON result that was converted from XML.""" respx.get("http://localhost").respond( @@ -722,8 +737,8 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): @respx.mock async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_template( - hass, -): + hass: HomeAssistant, +) -> None: """Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type.""" respx.get("http://localhost").respond( @@ -759,7 +774,9 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp @respx.mock -async def test_update_with_xml_convert_bad_xml(hass, caplog): +async def test_update_with_xml_convert_bad_xml( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a XML result with bad xml.""" respx.get("http://localhost").respond( @@ -794,7 +811,9 @@ async def test_update_with_xml_convert_bad_xml(hass, caplog): @respx.mock -async def test_update_with_failed_get(hass, caplog): +async def test_update_with_failed_get( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a XML result with bad xml.""" respx.get("http://localhost").respond( @@ -829,7 +848,7 @@ async def test_update_with_failed_get(hass, caplog): @respx.mock -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload reset sensors.""" respx.get("http://localhost") % HTTPStatus.OK diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index a3c0f78db1c..ae7d507e857 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -3,324 +3,429 @@ import asyncio from http import HTTPStatus import aiohttp +import pytest from homeassistant.components.rest import DOMAIN -import homeassistant.components.rest.switch as rest -from homeassistant.components.switch import SwitchDeviceClass +from homeassistant.components.rest.switch import ( + CONF_BODY_OFF, + CONF_BODY_ON, + CONF_STATE_RESOURCE, +) +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SCAN_INTERVAL, + SwitchDeviceClass, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + ATTR_ICON, CONF_DEVICE_CLASS, CONF_HEADERS, + CONF_ICON, + CONF_METHOD, CONF_NAME, CONF_PARAMS, CONF_PLATFORM, CONF_RESOURCE, + CONF_UNIQUE_ID, CONTENT_TYPE_JSON, - Platform, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import CONF_PICTURE from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from tests.common import assert_setup_component +from tests.common import assert_setup_component, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker NAME = "foo" DEVICE_CLASS = SwitchDeviceClass.SWITCH -METHOD = "post" RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE -PARAMS = None -async def test_setup_missing_config(hass): +async def test_setup_missing_config( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup with configuration missing required entries.""" - assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "Invalid config for [switch.rest]: required key not provided" in caplog.text -async def test_setup_missing_schema(hass): +async def test_setup_missing_schema( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup with resource missing schema.""" - assert not await rest.async_setup_platform( - hass, - {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"}, - None, - ) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "Invalid config for [switch.rest]: invalid url" in caplog.text -async def test_setup_failed_connect(hass, aioclient_mock): +async def test_setup_failed_connect( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, +) -> None: """Test setup when connection error occurs.""" - aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) - assert not await rest.async_setup_platform( - hass, - {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"}, - None, - ) + aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "No route to resource/endpoint" in caplog.text -async def test_setup_timeout(hass, aioclient_mock): +async def test_setup_timeout( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, +) -> None: """Test setup when connection timeout occurs.""" - aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) - assert not await rest.async_setup_platform( - hass, - {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"}, - None, - ) + aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "No route to resource/endpoint" in caplog.text -async def test_setup_minimum(hass, aioclient_mock): +async def test_setup_minimum( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with minimum configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) - with assert_setup_component(1, Platform.SWITCH): - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_RESOURCE: "http://localhost", - } - }, - ) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 -async def test_setup_query_params(hass, aioclient_mock): +async def test_setup_query_params( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with query params.""" aioclient_mock.get("http://localhost/?search=something", status=HTTPStatus.OK) - with assert_setup_component(1, Platform.SWITCH): - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_RESOURCE: "http://localhost", - CONF_PARAMS: {"search": "something"}, - } - }, - ) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_RESOURCE: RESOURCE, + CONF_PARAMS: {"search": "something"}, + } + } + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 -async def test_setup(hass, aioclient_mock): +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_NAME: "foo", - CONF_RESOURCE: "http://localhost", - CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, - rest.CONF_BODY_ON: "custom on text", - rest.CONF_BODY_OFF: "custom off text", - } - }, - ) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: RESOURCE, + CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, + CONF_BODY_ON: "custom on text", + CONF_BODY_OFF: "custom off text", + } + } + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 - assert_setup_component(1, Platform.SWITCH) + assert_setup_component(1, SWITCH_DOMAIN) -async def test_setup_with_state_resource(hass, aioclient_mock): +async def test_setup_with_state_resource( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.NOT_FOUND) + aioclient_mock.get(RESOURCE, status=HTTPStatus.NOT_FOUND) aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK) - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_NAME: "foo", - CONF_RESOURCE: "http://localhost", - rest.CONF_STATE_RESOURCE: "http://localhost/state", - CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, - rest.CONF_BODY_ON: "custom on text", - rest.CONF_BODY_OFF: "custom off text", - } - }, - ) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: RESOURCE, + CONF_STATE_RESOURCE: "http://localhost/state", + CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, + CONF_BODY_ON: "custom on text", + CONF_BODY_OFF: "custom off text", + } + } + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 - assert_setup_component(1, Platform.SWITCH) + assert_setup_component(1, SWITCH_DOMAIN) -async def test_setup_with_templated_headers_params(hass, aioclient_mock): +async def test_setup_with_templated_headers_params( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_NAME: "foo", - CONF_RESOURCE: "http://localhost", - CONF_HEADERS: { - "Accept": CONTENT_TYPE_JSON, - "User-Agent": "Mozilla/{{ 3 + 2 }}.0", - }, - CONF_PARAMS: { - "start": 0, - "end": "{{ 3 + 2 }}", - }, - } - }, - ) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: "http://localhost", + CONF_HEADERS: { + "Accept": CONTENT_TYPE_JSON, + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + }, + CONF_PARAMS: { + "start": 0, + "end": "{{ 3 + 2 }}", + }, + } + } + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 assert aioclient_mock.mock_calls[-1][3].get("Accept") == CONTENT_TYPE_JSON assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0" assert aioclient_mock.mock_calls[-1][1].query["start"] == "0" assert aioclient_mock.mock_calls[-1][1].query["end"] == "5" - assert_setup_component(1, Platform.SWITCH) + assert_setup_component(1, SWITCH_DOMAIN) -"""Tests for REST switch platform.""" +# Tests for REST switch platform. -def _setup_test_switch(hass): - body_on = Template("on", hass) - body_off = Template("off", hass) - headers = {"Content-type": Template(CONTENT_TYPE_JSON, hass)} - switch = rest.RestSwitch( - hass, - { - CONF_NAME: Template(NAME, hass), - CONF_DEVICE_CLASS: DEVICE_CLASS, - CONF_RESOURCE: RESOURCE, - rest.CONF_STATE_RESOURCE: STATE_RESOURCE, - rest.CONF_METHOD: METHOD, - rest.CONF_HEADERS: headers, - rest.CONF_PARAMS: PARAMS, - rest.CONF_BODY_ON: body_on, - rest.CONF_BODY_OFF: body_off, - rest.CONF_IS_ON_TEMPLATE: None, - rest.CONF_TIMEOUT: 10, - rest.CONF_VERIFY_SSL: True, - }, - None, - ) - switch.hass = hass - return switch, body_on, body_off +async def _async_setup_test_switch( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + + headers = {"Content-type": CONTENT_TYPE_JSON} + config = { + CONF_PLATFORM: DOMAIN, + CONF_NAME: NAME, + CONF_DEVICE_CLASS: DEVICE_CLASS, + CONF_RESOURCE: RESOURCE, + CONF_STATE_RESOURCE: STATE_RESOURCE, + CONF_HEADERS: headers, + } + + assert await async_setup_component(hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: config}) + await hass.async_block_till_done() + assert_setup_component(1, SWITCH_DOMAIN) + + assert hass.states.get("switch.foo").state == STATE_UNKNOWN + aioclient_mock.clear_requests() -def test_name(hass): +async def test_name(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test the name.""" - switch, body_on, body_off = _setup_test_switch(hass) - assert switch.name == NAME + await _async_setup_test_switch(hass, aioclient_mock) + + state = hass.states.get("switch.foo") + assert state.attributes[ATTR_FRIENDLY_NAME] == NAME -def test_device_class(hass): - """Test the name.""" - switch, body_on, body_off = _setup_test_switch(hass) - assert switch.device_class == DEVICE_CLASS +async def test_device_class( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the device class.""" + await _async_setup_test_switch(hass, aioclient_mock) + + state = hass.states.get("switch.foo") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS -def test_is_on_before_update(hass): +async def test_is_on_before_update( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test is_on in initial state.""" - switch, body_on, body_off = _setup_test_switch(hass) - assert switch.is_on is None + await _async_setup_test_switch(hass, aioclient_mock) + + state = hass.states.get("switch.foo") + assert state.state == STATE_UNKNOWN -async def test_turn_on_success(hass, aioclient_mock): +async def test_turn_on_success( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_on.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert body_on.template == aioclient_mock.mock_calls[-1][2].decode() - assert switch.is_on + assert aioclient_mock.mock_calls[-2][2].decode() == "ON" + assert hass.states.get("switch.foo").state == STATE_ON -async def test_turn_on_status_not_ok(hass, aioclient_mock): +async def test_turn_on_status_not_ok( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_on when error status returned.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert body_on.template == aioclient_mock.mock_calls[-1][2].decode() - assert switch.is_on is None + assert aioclient_mock.mock_calls[-1][2].decode() == "ON" + assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_turn_on_timeout(hass, aioclient_mock): +async def test_turn_on_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_on when timeout occurs.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert switch.is_on is None + assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_turn_off_success(hass, aioclient_mock): +async def test_turn_off_success( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_off.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_off() + aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert body_off.template == aioclient_mock.mock_calls[-1][2].decode() - assert not switch.is_on + assert aioclient_mock.mock_calls[-2][2].decode() == "OFF" + + assert hass.states.get("switch.foo").state == STATE_OFF -async def test_turn_off_status_not_ok(hass, aioclient_mock): +async def test_turn_off_status_not_ok( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_off when error status returned.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_off() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert body_off.template == aioclient_mock.mock_calls[-1][2].decode() - assert switch.is_on is None + assert aioclient_mock.mock_calls[-1][2].decode() == "OFF" + + assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_turn_off_timeout(hass, aioclient_mock): +async def test_turn_off_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_off when timeout occurs.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, exc=asyncio.TimeoutError()) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert switch.is_on is None + assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_update_when_on(hass, aioclient_mock): +async def test_update_when_on( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when switch is on.""" - switch, body_on, body_off = _setup_test_switch(hass) - aioclient_mock.get(RESOURCE, text=body_on.template) - await switch.async_update() + await _async_setup_test_switch(hass, aioclient_mock) - assert switch.is_on + aioclient_mock.get(RESOURCE, text="ON") + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert hass.states.get("switch.foo").state == STATE_ON -async def test_update_when_off(hass, aioclient_mock): +async def test_update_when_off( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when switch is off.""" - switch, body_on, body_off = _setup_test_switch(hass) - aioclient_mock.get(RESOURCE, text=body_off.template) - await switch.async_update() + await _async_setup_test_switch(hass, aioclient_mock) - assert not switch.is_on + aioclient_mock.get(RESOURCE, text="OFF") + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert hass.states.get("switch.foo").state == STATE_OFF -async def test_update_when_unknown(hass, aioclient_mock): +async def test_update_when_unknown( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when unknown status returned.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.get(RESOURCE, text="unknown status") - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_update() + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() - assert switch.is_on is None + assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_update_timeout(hass, aioclient_mock): +async def test_update_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when timeout occurs.""" - aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_update() + await _async_setup_test_switch(hass, aioclient_mock) - assert switch.is_on is None + aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert hass.states.get("switch.foo").state == STATE_UNKNOWN async def test_entity_config( @@ -328,22 +433,22 @@ async def test_entity_config( ) -> None: """Test entity configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) config = { - Platform.SWITCH: { + SWITCH_DOMAIN: { # REST configuration - "platform": "rest", - "method": "POST", - "resource": "http://localhost", + CONF_PLATFORM: "rest", + CONF_METHOD: "POST", + CONF_RESOURCE: "http://localhost", # Entity configuration - "icon": "{{'mdi:one_two_three'}}", - "picture": "{{'blabla.png'}}", - "name": "{{'REST' + ' ' + 'Switch'}}", - "unique_id": "very_unique", + CONF_ICON: "{{'mdi:one_two_three'}}", + CONF_PICTURE: "{{'blabla.png'}}", + CONF_NAME: "{{'REST' + ' ' + 'Switch'}}", + CONF_UNIQUE_ID: "very_unique", }, } - assert await async_setup_component(hass, Platform.SWITCH, config) + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() entity_registry = er.async_get(hass) @@ -352,7 +457,7 @@ async def test_entity_config( state = hass.states.get("switch.rest_switch") assert state.state == "unknown" assert state.attributes == { - "entity_picture": "blabla.png", - "friendly_name": "REST Switch", - "icon": "mdi:one_two_three", + ATTR_ENTITY_PICTURE: "blabla.png", + ATTR_FRIENDLY_NAME: "REST Switch", + ATTR_ICON: "mdi:one_two_three", } diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 73def144aba..cd0dfe094d4 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -25,6 +25,7 @@ from homeassistant.const import ( SERVICE_STOP_COVER, SERVICE_TURN_OFF, ) +from homeassistant.helpers import entity_registry as er async def mock_rflink( @@ -476,3 +477,40 @@ async def test_default_keepalive(hass, monkeypatch, caplog): == DEFAULT_TCP_KEEPALIVE_IDLE_TIMER ) # no keepalive config will default it assert "TCP Keepalive IDLE timer was provided" not in caplog.text + + +async def test_unique_id(hass, monkeypatch): + """Validate the device unique_id.""" + + DOMAIN = "sensor" + config = { + "rflink": {"port": "/dev/ttyABC0"}, + DOMAIN: { + "platform": "rflink", + "devices": { + "my_humidity_device_unique_id": { + "name": "humidity_device", + "sensor_type": "humidity", + "aliases": ["test_alias_02_0"], + }, + "my_temperature_device_unique_id": { + "name": "temperature_device", + "sensor_type": "temperature", + "aliases": ["test_alias_02_0"], + }, + }, + }, + } + + registry = er.async_get(hass) + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch) + + humidity_entry = registry.async_get("sensor.humidity_device") + assert humidity_entry + assert humidity_entry.unique_id == "my_humidity_device_unique_id" + + temperature_entry = registry.async_get("sensor.temperature_device") + assert temperature_entry + assert temperature_entry.unique_id == "my_temperature_device_unique_id" diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 13d7e4b300d..0202894a41c 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -12,11 +12,14 @@ from homeassistant.components.rflink import ( EVENT_KEY_SENSOR, TMP_ENTITY, ) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, + PRECIPITATION_MILLIMETERS, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from .test_init import mock_rflink @@ -47,11 +50,18 @@ async def test_default_setup(hass, monkeypatch): config_sensor = hass.states.get("sensor.test") assert config_sensor assert config_sensor.state == "unknown" - assert config_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ( + config_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + ) # test event for config sensor event_callback( - {"id": "test", "sensor": "temperature", "value": 1, "unit": TEMP_CELSIUS} + { + "id": "test", + "sensor": "temperature", + "value": 1, + "unit": UnitOfTemperature.CELSIUS, + } ) await hass.async_block_till_done() @@ -59,16 +69,34 @@ async def test_default_setup(hass, monkeypatch): # test event for new unconfigured sensor event_callback( - {"id": "test2", "sensor": "temperature", "value": 0, "unit": TEMP_CELSIUS} + { + "id": "test2", + "sensor": "temperature", + "value": 0, + "unit": UnitOfTemperature.CELSIUS, + } ) await hass.async_block_till_done() - # test state of new sensor - new_sensor = hass.states.get("sensor.test2") - assert new_sensor - assert new_sensor.state == "0" - assert new_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert new_sensor.attributes["icon"] == "mdi:thermometer" + # test state of temp sensor + temp_sensor = hass.states.get("sensor.test2") + assert temp_sensor + assert temp_sensor.state == "0" + assert temp_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert ( + ATTR_ICON not in temp_sensor.attributes + ) # temperature uses SensorEntityDescription + + # test event for new unconfigured sensor + event_callback({"id": "test3", "sensor": "battery", "value": "ok", "unit": None}) + await hass.async_block_till_done() + + # test state of battery sensor + bat_sensor = hass.states.get("sensor.test3") + assert bat_sensor + assert bat_sensor.state == "ok" + assert ATTR_UNIT_OF_MEASUREMENT not in bat_sensor.attributes + assert bat_sensor.attributes[ATTR_ICON] == "mdi:battery" async def test_disable_automatic_add(hass, monkeypatch): @@ -83,7 +111,12 @@ async def test_disable_automatic_add(hass, monkeypatch): # test event for new unconfigured sensor event_callback( - {"id": "test2", "sensor": "temperature", "value": 0, "unit": TEMP_CELSIUS} + { + "id": "test2", + "sensor": "temperature", + "value": 0, + "unit": UnitOfTemperature.CELSIUS, + } ) await hass.async_block_till_done() @@ -161,7 +194,7 @@ async def test_aliases(hass, monkeypatch): ) await hass.async_block_till_done() - # test state of new sensor + # test state of new sensor updated_sensor = hass.states.get("sensor.test_02") assert updated_sensor assert updated_sensor.state == "65" @@ -187,11 +220,11 @@ async def test_race_condition(hass, monkeypatch): await hass.async_block_till_done() - # test state of new sensor + # test state of new sensor updated_sensor = hass.states.get("sensor.test3") assert updated_sensor - # test state of new sensor + # test state of new sensor new_sensor = hass.states.get(f"{DOMAIN}.test3") assert new_sensor assert new_sensor.state == "ok" @@ -201,7 +234,77 @@ async def test_race_condition(hass, monkeypatch): # tmp_entity must be deleted from EVENT_KEY_COMMAND assert tmp_entity not in hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_SENSOR]["test3"] - # test state of new sensor + # test state of new sensor new_sensor = hass.states.get(f"{DOMAIN}.test3") assert new_sensor assert new_sensor.state == "ko" + + +async def test_sensor_attributes(hass, monkeypatch): + """Validate the sensor attributes.""" + + config = { + "rflink": {"port": "/dev/ttyABC0"}, + DOMAIN: { + "platform": "rflink", + "devices": { + "my_meter_device_unique_id": { + "name": "meter_device", + "sensor_type": "meter_value", + }, + "my_rain_device_unique_id": { + "name": "rain_device", + "sensor_type": "total_rain", + }, + "my_humidity_device_unique_id": { + "name": "humidity_device", + "sensor_type": "humidity", + }, + "my_temperature_device_unique_id": { + "name": "temperature_device", + "sensor_type": "temperature", + }, + "another_temperature_device_unique_id": { + "name": "fahrenheit_device", + "sensor_type": "temperature", + "unit_of_measurement": "F", + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch) + + # test sensor loaded from config + meter_state = hass.states.get("sensor.meter_device") + assert meter_state + assert "device_class" not in meter_state.attributes + assert "state_class" not in meter_state.attributes + assert "unit_of_measurement" not in meter_state.attributes + + rain_state = hass.states.get("sensor.rain_device") + assert rain_state + assert rain_state.attributes["device_class"] == SensorDeviceClass.PRECIPITATION + assert rain_state.attributes["state_class"] == SensorStateClass.TOTAL_INCREASING + assert rain_state.attributes["unit_of_measurement"] == PRECIPITATION_MILLIMETERS + + humidity_state = hass.states.get("sensor.humidity_device") + assert humidity_state + assert humidity_state.attributes["device_class"] == SensorDeviceClass.HUMIDITY + assert humidity_state.attributes["state_class"] == SensorStateClass.MEASUREMENT + assert humidity_state.attributes["unit_of_measurement"] == PERCENTAGE + + temperature_state = hass.states.get("sensor.temperature_device") + assert temperature_state + assert temperature_state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE + assert temperature_state.attributes["state_class"] == SensorStateClass.MEASUREMENT + assert ( + temperature_state.attributes["unit_of_measurement"] == UnitOfTemperature.CELSIUS + ) + + fahrenheit_state = hass.states.get("sensor.fahrenheit_device") + assert fahrenheit_state + assert fahrenheit_state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE + assert fahrenheit_state.attributes["state_class"] == SensorStateClass.MEASUREMENT + assert fahrenheit_state.attributes["unit_of_measurement"] == "F" diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index 006e57b9ae5..9d14cfdcff0 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -39,10 +39,14 @@ def two_zone_cloud(): zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) ), patch.object( zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), patch.object( + zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) ), patch.object( zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) ), patch.object( zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), patch.object( + zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) ), patch.object( alarm_mock, "zones", @@ -54,6 +58,40 @@ def two_zone_cloud(): yield zone_mocks +@fixture +def two_zone_local(): + """Fixture to mock alarm with two zones.""" + zone_mocks = {0: zone_mock(), 1: zone_mock()} + with patch.object( + zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), patch.object( + zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[0], "armed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch.object( + zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), patch.object( + zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[1], "armed", new_callable=PropertyMock(return_value=False) + ), patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value={}), + ), patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value=zone_mocks), + ): + yield zone_mocks + + @fixture def options(): """Fixture for default (empty) options.""" @@ -70,7 +108,10 @@ def events(): def cloud_config_entry(hass, options): """Fixture for a cloud config entry.""" config_entry = MockConfigEntry( - domain=DOMAIN, data=TEST_CLOUD_CONFIG, options=options + domain=DOMAIN, + data=TEST_CLOUD_CONFIG, + options=options, + unique_id=TEST_CLOUD_CONFIG[CONF_USERNAME], ) config_entry.add_to_hass(hass) return config_entry diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 71cbd04f391..1c331adc145 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -9,38 +9,14 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from .util import TEST_SITE_UUID, zone_mock +from .util import TEST_SITE_UUID FIRST_ENTITY_ID = "binary_sensor.zone_0" SECOND_ENTITY_ID = "binary_sensor.zone_1" FIRST_ALARMED_ENTITY_ID = FIRST_ENTITY_ID + "_alarmed" SECOND_ALARMED_ENTITY_ID = SECOND_ENTITY_ID + "_alarmed" - - -@pytest.fixture -def two_zone_local(): - """Fixture to mock alarm with two zones.""" - zone_mocks = {0: zone_mock(), 1: zone_mock()} - with patch.object( - zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) - ), patch.object( - zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") - ), patch.object( - zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) - ), patch.object( - zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") - ), patch.object( - zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False) - ), patch( - "homeassistant.components.risco.RiscoLocal.partitions", - new_callable=PropertyMock(return_value={}), - ), patch( - "homeassistant.components.risco.RiscoLocal.zones", - new_callable=PropertyMock(return_value=zone_mocks), - ): - yield zone_mocks +FIRST_ARMED_ENTITY_ID = FIRST_ENTITY_ID + "_armed" +SECOND_ARMED_ENTITY_ID = SECOND_ENTITY_ID + "_armed" @pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) @@ -69,59 +45,26 @@ async def test_cloud_setup(hass, two_zone_cloud, setup_risco_cloud): assert device.manufacturer == "Risco" -async def _check_cloud_state(hass, zones, triggered, bypassed, entity_id, zone_id): +async def _check_cloud_state(hass, zones, triggered, entity_id, zone_id): with patch.object( zones[zone_id], "triggered", new_callable=PropertyMock(return_value=triggered), - ), patch.object( - zones[zone_id], - "bypassed", - new_callable=PropertyMock(return_value=bypassed), ): await async_update_entity(hass, entity_id) await hass.async_block_till_done() expected_triggered = STATE_ON if triggered else STATE_OFF assert hass.states.get(entity_id).state == expected_triggered - assert hass.states.get(entity_id).attributes["bypassed"] == bypassed assert hass.states.get(entity_id).attributes["zone_id"] == zone_id async def test_cloud_states(hass, two_zone_cloud, setup_risco_cloud): """Test the various alarm states.""" - await _check_cloud_state(hass, two_zone_cloud, True, True, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, True, False, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, False, True, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, False, False, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, True, True, SECOND_ENTITY_ID, 1) - await _check_cloud_state(hass, two_zone_cloud, True, False, SECOND_ENTITY_ID, 1) - await _check_cloud_state(hass, two_zone_cloud, False, True, SECOND_ENTITY_ID, 1) - await _check_cloud_state(hass, two_zone_cloud, False, False, SECOND_ENTITY_ID, 1) - - -async def test_cloud_bypass(hass, two_zone_cloud, setup_risco_cloud): - """Test bypassing a zone.""" - with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: - data = {"entity_id": FIRST_ENTITY_ID} - - await hass.services.async_call( - DOMAIN, "bypass_zone", service_data=data, blocking=True - ) - - mock.assert_awaited_once_with(0, True) - - -async def test_cloud_unbypass(hass, two_zone_cloud, setup_risco_cloud): - """Test unbypassing a zone.""" - with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: - data = {"entity_id": FIRST_ENTITY_ID} - - await hass.services.async_call( - DOMAIN, "unbypass_zone", service_data=data, blocking=True - ) - - mock.assert_awaited_once_with(0, False) + await _check_cloud_state(hass, two_zone_cloud, True, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, False, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, True, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, False, SECOND_ENTITY_ID, 1) @pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) @@ -155,39 +98,18 @@ async def test_local_setup(hass, two_zone_local, setup_risco_local): async def _check_local_state( - hass, zones, triggered, bypassed, entity_id, zone_id, callback + hass, zones, property, value, entity_id, zone_id, callback ): with patch.object( zones[zone_id], - "triggered", - new_callable=PropertyMock(return_value=triggered), - ), patch.object( - zones[zone_id], - "bypassed", - new_callable=PropertyMock(return_value=bypassed), + property, + new_callable=PropertyMock(return_value=value), ): await callback(zone_id, zones[zone_id]) await hass.async_block_till_done() - expected_triggered = STATE_ON if triggered else STATE_OFF - assert hass.states.get(entity_id).state == expected_triggered - assert hass.states.get(entity_id).attributes["bypassed"] == bypassed - assert hass.states.get(entity_id).attributes["zone_id"] == zone_id - - -async def _check_alarmed_local_state( - hass, zones, alarmed, entity_id, zone_id, callback -): - with patch.object( - zones[zone_id], - "alarmed", - new_callable=PropertyMock(return_value=alarmed), - ): - await callback(zone_id, zones[zone_id]) - await hass.async_block_till_done() - - expected_alarmed = STATE_ON if alarmed else STATE_OFF - assert hass.states.get(entity_id).state == expected_alarmed + expected_value = STATE_ON if value else STATE_OFF + assert hass.states.get(entity_id).state == expected_value assert hass.states.get(entity_id).attributes["zone_id"] == zone_id @@ -200,78 +122,64 @@ def _mock_zone_handler(): async def test_local_states( hass, two_zone_local, _mock_zone_handler, setup_risco_local ): - """Test the various alarm states.""" + """Test the various zone states.""" callback = _mock_zone_handler.call_args.args[0] assert callback is not None await _check_local_state( - hass, two_zone_local, True, True, FIRST_ENTITY_ID, 0, callback + hass, two_zone_local, "triggered", True, FIRST_ENTITY_ID, 0, callback ) await _check_local_state( - hass, two_zone_local, True, False, FIRST_ENTITY_ID, 0, callback + hass, two_zone_local, "triggered", False, FIRST_ENTITY_ID, 0, callback ) await _check_local_state( - hass, two_zone_local, False, True, FIRST_ENTITY_ID, 0, callback + hass, two_zone_local, "triggered", True, SECOND_ENTITY_ID, 1, callback ) await _check_local_state( - hass, two_zone_local, False, False, FIRST_ENTITY_ID, 0, callback - ) - await _check_local_state( - hass, two_zone_local, True, True, SECOND_ENTITY_ID, 1, callback - ) - await _check_local_state( - hass, two_zone_local, True, False, SECOND_ENTITY_ID, 1, callback - ) - await _check_local_state( - hass, two_zone_local, False, True, SECOND_ENTITY_ID, 1, callback - ) - await _check_local_state( - hass, two_zone_local, False, False, SECOND_ENTITY_ID, 1, callback + hass, two_zone_local, "triggered", False, SECOND_ENTITY_ID, 1, callback ) async def test_alarmed_local_states( hass, two_zone_local, _mock_zone_handler, setup_risco_local ): - """Test the various alarm states.""" + """Test the various zone alarmed states.""" callback = _mock_zone_handler.call_args.args[0] assert callback is not None - await _check_alarmed_local_state( - hass, two_zone_local, True, FIRST_ALARMED_ENTITY_ID, 0, callback + await _check_local_state( + hass, two_zone_local, "alarmed", True, FIRST_ALARMED_ENTITY_ID, 0, callback ) - await _check_alarmed_local_state( - hass, two_zone_local, False, FIRST_ALARMED_ENTITY_ID, 0, callback + await _check_local_state( + hass, two_zone_local, "alarmed", False, FIRST_ALARMED_ENTITY_ID, 0, callback ) - await _check_alarmed_local_state( - hass, two_zone_local, True, SECOND_ALARMED_ENTITY_ID, 1, callback + await _check_local_state( + hass, two_zone_local, "alarmed", True, SECOND_ALARMED_ENTITY_ID, 1, callback ) - await _check_alarmed_local_state( - hass, two_zone_local, False, SECOND_ALARMED_ENTITY_ID, 1, callback + await _check_local_state( + hass, two_zone_local, "alarmed", False, SECOND_ALARMED_ENTITY_ID, 1, callback ) -async def test_local_bypass(hass, two_zone_local, setup_risco_local): - """Test bypassing a zone.""" - with patch.object(two_zone_local[0], "bypass") as mock: - data = {"entity_id": FIRST_ENTITY_ID} +async def test_armed_local_states( + hass, two_zone_local, _mock_zone_handler, setup_risco_local +): + """Test the various zone armed states.""" + callback = _mock_zone_handler.call_args.args[0] - await hass.services.async_call( - DOMAIN, "bypass_zone", service_data=data, blocking=True - ) + assert callback is not None - mock.assert_awaited_once_with(True) - - -async def test_local_unbypass(hass, two_zone_local, setup_risco_local): - """Test unbypassing a zone.""" - with patch.object(two_zone_local[0], "bypass") as mock: - data = {"entity_id": FIRST_ENTITY_ID} - - await hass.services.async_call( - DOMAIN, "unbypass_zone", service_data=data, blocking=True - ) - - mock.assert_awaited_once_with(False) + await _check_local_state( + hass, two_zone_local, "armed", True, FIRST_ARMED_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, "armed", False, FIRST_ARMED_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, "armed", True, SECOND_ARMED_ENTITY_ID, 1, callback + ) + await _check_local_state( + hass, two_zone_local, "armed", False, SECOND_ARMED_ENTITY_ID, 1, callback + ) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 396aad8015d..0c71ba9efdc 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components.risco.config_flow import ( UnauthorizedError, ) from homeassistant.components.risco.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -142,6 +143,75 @@ async def test_form_cloud_already_exists(hass): assert result3["reason"] == "already_configured" +async def test_form_reauth(hass, cloud_config_entry): + """Test reauthenticate.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=cloud_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ), patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**TEST_CLOUD_DATA, CONF_PASSWORD: "new_password"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert cloud_config_entry.data[CONF_PASSWORD] == "new_password" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_reauth_with_new_username(hass, cloud_config_entry): + """Test reauthenticate with new username.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=cloud_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ), patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**TEST_CLOUD_DATA, CONF_USERNAME: "new_user"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert cloud_config_entry.data[CONF_USERNAME] == "new_user" + assert cloud_config_entry.unique_id == "new_user" + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_local_form(hass): """Test we get the local form.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/risco/test_switch.py b/tests/components/risco/test_switch.py new file mode 100644 index 00000000000..5ea4e72abca --- /dev/null +++ b/tests/components/risco/test_switch.py @@ -0,0 +1,151 @@ +"""Tests for the Risco binary sensors.""" +from unittest.mock import PropertyMock, patch + +import pytest + +from homeassistant.components.risco import CannotConnectError, UnauthorizedError +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity + +FIRST_ENTITY_ID = "switch.zone_0_bypassed" +SECOND_ENTITY_ID = "switch.zone_1_bypassed" + + +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_login(hass, login_with_error, cloud_config_entry): + """Test error on login.""" + await hass.config_entries.async_setup(cloud_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_cloud_setup(hass, two_zone_cloud, setup_risco_cloud): + """Test entity setup.""" + registry = er.async_get(hass) + assert registry.async_is_registered(FIRST_ENTITY_ID) + assert registry.async_is_registered(SECOND_ENTITY_ID) + + +async def _check_cloud_state(hass, zones, bypassed, entity_id, zone_id): + with patch.object( + zones[zone_id], + "bypassed", + new_callable=PropertyMock(return_value=bypassed), + ): + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + + expected_bypassed = STATE_ON if bypassed else STATE_OFF + assert hass.states.get(entity_id).state == expected_bypassed + assert hass.states.get(entity_id).attributes["zone_id"] == zone_id + + +async def test_cloud_states(hass, two_zone_cloud, setup_risco_cloud): + """Test the various alarm states.""" + await _check_cloud_state(hass, two_zone_cloud, True, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, False, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, True, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, False, SECOND_ENTITY_ID, 1) + + +async def test_cloud_bypass(hass, two_zone_cloud, setup_risco_cloud): + """Test bypassing a zone.""" + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(0, True) + + +async def test_cloud_unbypass(hass, two_zone_cloud, setup_risco_cloud): + """Test unbypassing a zone.""" + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(0, False) + + +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_connect(hass, connect_with_error, local_config_entry): + """Test error on connect.""" + await hass.config_entries.async_setup(local_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_local_setup(hass, two_zone_local, setup_risco_local): + """Test entity setup.""" + registry = er.async_get(hass) + assert registry.async_is_registered(FIRST_ENTITY_ID) + assert registry.async_is_registered(SECOND_ENTITY_ID) + + +async def _check_local_state(hass, zones, bypassed, entity_id, zone_id, callback): + with patch.object( + zones[zone_id], + "bypassed", + new_callable=PropertyMock(return_value=bypassed), + ): + await callback(zone_id, zones[zone_id]) + await hass.async_block_till_done() + + expected_bypassed = STATE_ON if bypassed else STATE_OFF + assert hass.states.get(entity_id).state == expected_bypassed + assert hass.states.get(entity_id).attributes["zone_id"] == zone_id + + +@pytest.fixture +def _mock_zone_handler(): + with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock: + yield mock + + +async def test_local_states( + hass, two_zone_local, _mock_zone_handler, setup_risco_local +): + """Test the various alarm states.""" + callback = _mock_zone_handler.call_args.args[0] + + assert callback is not None + + await _check_local_state(hass, two_zone_local, True, FIRST_ENTITY_ID, 0, callback) + await _check_local_state(hass, two_zone_local, False, FIRST_ENTITY_ID, 0, callback) + await _check_local_state(hass, two_zone_local, True, SECOND_ENTITY_ID, 1, callback) + await _check_local_state(hass, two_zone_local, False, SECOND_ENTITY_ID, 1, callback) + + +async def test_local_bypass(hass, two_zone_local, setup_risco_local): + """Test bypassing a zone.""" + with patch.object(two_zone_local[0], "bypass") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(True) + + +async def test_local_unbypass(hass, two_zone_local, setup_risco_local): + """Test unbypassing a zone.""" + with patch.object(two_zone_local[0], "bypass") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(False) diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index ffdb4e5ba9a..0ed22ac84fa 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -8,8 +8,9 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_http_client(loop, hass, hass_client): +def mock_http_client(event_loop, hass, hass_client): """Set up test fixture.""" + loop = event_loop config = { "rss_feed_template": { "testfeed": { diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index 5a0d6de01df..f6ee0d1a628 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -1,4 +1,4 @@ -"""Tests for RTSPtoWebRTC inititalization.""" +"""Tests for RTSPtoWebRTC initialization.""" from __future__ import annotations diff --git a/tests/components/rtsp_to_webrtc/test_init.py b/tests/components/rtsp_to_webrtc/test_init.py index afa365a3044..abbe3728a12 100644 --- a/tests/components/rtsp_to_webrtc/test_init.py +++ b/tests/components/rtsp_to_webrtc/test_init.py @@ -1,4 +1,4 @@ -"""Tests for RTSPtoWebRTC inititalization.""" +"""Tests for RTSPtoWebRTC initialization.""" from __future__ import annotations diff --git a/tests/components/ruuvitag_ble/__init__.py b/tests/components/ruuvitag_ble/__init__.py new file mode 100644 index 00000000000..e39900689cc --- /dev/null +++ b/tests/components/ruuvitag_ble/__init__.py @@ -0,0 +1 @@ +"""Test package for RuuviTag BLE sensor integration.""" diff --git a/tests/components/ruuvitag_ble/fixtures.py b/tests/components/ruuvitag_ble/fixtures.py new file mode 100644 index 00000000000..26eee1bac5e --- /dev/null +++ b/tests/components/ruuvitag_ble/fixtures.py @@ -0,0 +1,26 @@ +"""Fixtures for testing RuuviTag BLE.""" +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( + name="RuuviTag 0911", + address="01:03:05:07:09:11", # Ignored (the payload encodes the correct MAC) + rssi=-60, + manufacturer_data={ + 1177: b"\x05\x05\xa0`\xa0\xc8\x9a\xfd4\x02\x8c\xff\x00cvriv\xde\xad{?\xef\xaf" + }, + service_data={}, + service_uuids=[], + source="local", +) +CONFIGURED_NAME = "RuuviTag EFAF" +CONFIGURED_PREFIX = "ruuvitag_efaf" diff --git a/tests/components/ruuvitag_ble/test_config_flow.py b/tests/components/ruuvitag_ble/test_config_flow.py new file mode 100644 index 00000000000..1482f9b61b0 --- /dev/null +++ b/tests/components/ruuvitag_ble/test_config_flow.py @@ -0,0 +1,202 @@ +"""Test the Ruuvitag config flow.""" + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.ruuvitag_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from .fixtures import CONFIGURED_NAME, NOT_RUUVITAG_SERVICE_INFO, RUUVITAG_SERVICE_INFO + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Mock bluetooth for all tests in this module.""" + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address + + +async def test_async_step_bluetooth_not_ruuvitag(hass): + """Test discovery via bluetooth not ruuvitag.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": RUUVITAG_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=RUUVITAG_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": RUUVITAG_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=RUUVITAG_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=RUUVITAG_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": RUUVITAG_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["data"] == {} + assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address diff --git a/tests/components/ruuvitag_ble/test_sensor.py b/tests/components/ruuvitag_ble/test_sensor.py new file mode 100644 index 00000000000..2f9c027293a --- /dev/null +++ b/tests/components/ruuvitag_ble/test_sensor.py @@ -0,0 +1,46 @@ +"""Test the Ruuvitag BLE sensors.""" + +from __future__ import annotations + +from homeassistant.components.ruuvitag_ble.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant + +from .fixtures import CONFIGURED_NAME, CONFIGURED_PREFIX, RUUVITAG_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors(enable_bluetooth, hass: HomeAssistant): + """Test the RuuviTag BLE sensors.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=RUUVITAG_SERVICE_INFO.address) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info( + hass, + RUUVITAG_SERVICE_INFO, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) >= 4 + + for sensor, value, unit, state_class in [ + ("temperature", "7.2", "°C", "measurement"), + ("humidity", "61.84", "%", "measurement"), + ("pressure", "1013.54", "hPa", "measurement"), + ("voltage", "2395", "mV", "measurement"), + ]: + state = hass.states.get(f"sensor.{CONFIGURED_PREFIX}_{sensor}") + assert state is not None + assert state.state == value + name_lower = state.attributes[ATTR_FRIENDLY_NAME].lower() + assert name_lower == f"{CONFIGURED_NAME} {sensor}".lower() + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit + assert state.attributes[ATTR_STATE_CLASS] == state_class + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py index 644ea84854a..b13cc4a7326 100644 --- a/tests/components/scrape/__init__.py +++ b/tests/components/scrape/__init__.py @@ -4,6 +4,31 @@ from __future__ import annotations from typing import Any +def return_integration_config( + *, + authentication=None, + username=None, + password=None, + headers=None, + sensors=None, +) -> dict[str, dict[str, Any]]: + """Return config.""" + config = { + "resource": "https://www.home-assistant.io", + "verify_ssl": True, + "sensor": sensors, + } + if authentication: + config["authentication"] = authentication + if username: + config["username"] = username + config["password"] = password + if headers: + config["headers"] = headers + + return config + + def return_config( select, name, @@ -19,6 +44,7 @@ def return_config( password=None, headers=None, unique_id=None, + remove_platform=False, ) -> dict[str, dict[str, Any]]: """Return config.""" config = { @@ -26,7 +52,11 @@ def return_config( "resource": "https://www.home-assistant.io", "select": select, "name": name, + "index": 0, + "verify_ssl": True, } + if remove_platform: + config.pop("platform") if attribute: config["attribute"] = attribute if index: @@ -41,6 +71,7 @@ def return_config( config["state_class"] = state_class if authentication: config["authentication"] = authentication + if username: config["username"] = username config["password"] = password if headers: @@ -67,11 +98,20 @@ class MockRestData: self.count += 1 if self.payload == "test_scrape_sensor": self.data = ( + # Default "
" "

Current Version: 2021.12.10

Released: January 17, 2022" "
" "" ) + if self.payload == "test_scrape_sensor2": + self.data = ( + # Hidden version + "
" + "

Hidden Version: 2021.12.10

Released: January 17, 2022" + "
" + "" + ) if self.payload == "test_scrape_uom_and_classes": self.data = ( "
" diff --git a/tests/components/scrape/conftest.py b/tests/components/scrape/conftest.py new file mode 100644 index 00000000000..fa90786ec2f --- /dev/null +++ b/tests/components/scrape/conftest.py @@ -0,0 +1,93 @@ +"""Fixtures for the Scrape integration.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import patch +import uuid + +import pytest + +from homeassistant.components.rest.data import DEFAULT_TIMEOUT +from homeassistant.components.rest.schema import DEFAULT_METHOD, DEFAULT_VERIFY_SSL +from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + CONF_METHOD, + CONF_NAME, + CONF_RESOURCE, + CONF_TIMEOUT, + CONF_UNIQUE_ID, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from . import MockRestData + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="get_config") +async def get_config_to_integration_load() -> dict[str, Any]: + """Return default minimal configuration. + + To override the config, tests can be marked with: + @pytest.mark.parametrize("get_config", [{...}]) + """ + return { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: DEFAULT_METHOD, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + CONF_TIMEOUT: DEFAULT_TIMEOUT, + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + } + ], + } + + +@pytest.fixture(name="get_data") +async def get_data_to_integration_load() -> MockRestData: + """Return RestData. + + To override the config, tests can be marked with: + @pytest.mark.parametrize("get_data", [{...}]) + """ + return MockRestData("test_scrape_sensor") + + +@pytest.fixture(name="loaded_entry") +async def load_integration( + hass: HomeAssistant, get_config: dict[str, Any], get_data: MockRestData +) -> MockConfigEntry: + """Set up the Scrape integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + options=get_config, + entry_id="1", + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.rest.RestData", + return_value=get_data, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture(autouse=True) +def uuid_fixture() -> str: + """Automatically path uuid generator.""" + with patch( + "homeassistant.components.scrape.config_flow.uuid.uuid1", + return_value=uuid.UUID("3699ef88-69e6-11ed-a1eb-0242ac120002"), + ): + yield diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py new file mode 100644 index 00000000000..f6df8a80f21 --- /dev/null +++ b/tests/components/scrape/test_config_flow.py @@ -0,0 +1,424 @@ +"""Test the Scrape config flow.""" +from __future__ import annotations + +from unittest.mock import patch +import uuid + +from homeassistant import config_entries +from homeassistant.components.rest.data import DEFAULT_TIMEOUT +from homeassistant.components.rest.schema import DEFAULT_METHOD +from homeassistant.components.scrape import DOMAIN +from homeassistant.components.scrape.const import ( + CONF_INDEX, + CONF_SELECT, + DEFAULT_VERIFY_SSL, +) +from homeassistant.const import ( + CONF_METHOD, + CONF_NAME, + CONF_PASSWORD, + CONF_RESOURCE, + CONF_TIMEOUT, + CONF_UNIQUE_ID, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import HomeAssistantError + +from . import MockRestData + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant, get_data: MockRestData) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.rest.RestData", + return_value=get_data, + ) as mock_data, patch( + "homeassistant.components.scrape.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + }, + ) + await hass.async_block_till_done() + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["version"] == 1 + assert result3["options"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + } + ], + } + + assert len(mock_data.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None: + """Test config flow error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.rest.RestData", + side_effect=HomeAssistantError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + }, + ) + + assert result2["errors"] == {"base": "resource_error"} + + with patch( + "homeassistant.components.rest.RestData", + return_value=MockRestData("test_scrape_sensor_no_data"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + }, + ) + + assert result2["errors"] == {"base": "resource_error"} + + with patch("homeassistant.components.rest.RestData", return_value=get_data,), patch( + "homeassistant.components.scrape.async_setup_entry", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + }, + ) + await hass.async_block_till_done() + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + }, + ) + await hass.async_block_till_done() + + assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["title"] == "https://www.home-assistant.io" + assert result4["options"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + } + ], + } + + +async def test_options_resource_flow( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test options flow for a resource.""" + + state = hass.states.get("sensor.current_version") + assert state.state == "Current Version: 2021.12.10" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "resource"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "resource" + + mocker = MockRestData("test_scrape_sensor2") + with patch("homeassistant.components.rest.RestData", return_value=mocker): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: DEFAULT_METHOD, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + CONF_TIMEOUT: DEFAULT_TIMEOUT, + CONF_USERNAME: "secret_username", + CONF_PASSWORD: "secret_password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + CONF_USERNAME: "secret_username", + CONF_PASSWORD: "secret_password", + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + } + ], + } + + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + state = hass.states.get("sensor.current_version") + assert state.state == "Hidden Version: 2021.12.10" + + +async def test_options_add_remove_sensor_flow( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test options flow to add and remove a sensor.""" + + state = hass.states.get("sensor.current_version") + assert state.state == "Current Version: 2021.12.10" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "add_sensor"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_sensor" + + mocker = MockRestData("test_scrape_sensor2") + with patch("homeassistant.components.rest.RestData", return_value=mocker), patch( + "homeassistant.components.scrape.config_flow.uuid.uuid1", + return_value=uuid.UUID("3699ef88-69e6-11ed-a1eb-0242ac120003"), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Template", + CONF_SELECT: "template", + CONF_INDEX: 0.0, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10, + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + }, + { + CONF_NAME: "Template", + CONF_SELECT: "template", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120003", + }, + ], + } + + await hass.async_block_till_done() + + # Check the entity was updated, with the new entity + assert len(hass.states.async_all()) == 2 + + # Check the state of the entity has changed as expected + state = hass.states.get("sensor.current_version") + assert state.state == "Hidden Version: 2021.12.10" + + state = hass.states.get("sensor.template") + assert state.state == "Trying to get" + + # Now remove the original sensor + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "remove_sensor"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "remove_sensor" + + mocker = MockRestData("test_scrape_sensor2") + with patch("homeassistant.components.rest.RestData", return_value=mocker): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_INDEX: ["0"], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10, + "sensor": [ + { + CONF_NAME: "Template", + CONF_SELECT: "template", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120003", + }, + ], + } + + await hass.async_block_till_done() + + # Check the original entity was removed, with only the new entity left + assert len(hass.states.async_all()) == 1 + + # Check the state of the new entity + state = hass.states.get("sensor.template") + assert state.state == "Trying to get" + + +async def test_options_edit_sensor_flow( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test options flow to edit a sensor.""" + + state = hass.states.get("sensor.current_version") + assert state.state == "Current Version: 2021.12.10" + + result = await hass.config_entries.options.async_init(loaded_entry.entry_id) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "select_edit_sensor"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_sensor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "edit_sensor" + + mocker = MockRestData("test_scrape_sensor2") + with patch("homeassistant.components.rest.RestData", return_value=mocker): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_SELECT: "template", + CONF_INDEX: 0.0, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10, + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: "template", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + }, + ], + } + + await hass.async_block_till_done() + + # Check the entity was updated + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + state = hass.states.get("sensor.current_version") + assert state.state == "Trying to get" diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py new file mode 100644 index 00000000000..9b6122d6010 --- /dev/null +++ b/tests/components/scrape/test_init.py @@ -0,0 +1,127 @@ +"""Test Scrape component setup process.""" +from __future__ import annotations + +from datetime import datetime +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.scrape.const import DEFAULT_SCAN_INTERVAL, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import MockRestData, return_integration_config + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_setup_config(hass: HomeAssistant) -> None: + """Test setup from yaml.""" + config = { + DOMAIN: [ + return_integration_config( + sensors=[{"select": ".current-version h1", "name": "HA version"}] + ) + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ) as mock_setup: + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state.state == "Current Version: 2021.12.10" + + assert len(mock_setup.mock_calls) == 1 + + +async def test_setup_no_data_fails_with_recovery( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setup entry no data fails and recovers.""" + config = { + DOMAIN: [ + return_integration_config( + sensors=[{"select": ".current-version h1", "name": "HA version"}] + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_no_data") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state is None + + assert "Platform scrape not ready yet" in caplog.text + + mocker.payload = "test_scrape_sensor" + async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state.state == "Current Version: 2021.12.10" + + +async def test_setup_config_no_configuration(hass: HomeAssistant) -> None: + """Test setup from yaml missing configuration options.""" + config = {DOMAIN: None} + + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + entities = er.async_get(hass) + assert entities.entities == {} + + +async def test_setup_config_no_sensors( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setup from yaml with no configured sensors finalize properly.""" + config = { + DOMAIN: [ + { + "resource": "https://www.address.com", + "verify_ssl": True, + }, + { + "resource": "https://www.address2.com", + "verify_ssl": True, + "sensor": None, + }, + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + +async def test_setup_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test setup entry.""" + + assert loaded_entry.state == config_entries.ConfigEntryState.LOADED + + +async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: + """Test unload an entry.""" + + assert loaded_entry.state == config_entries.ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(loaded_entry.entry_id) + await hass.async_block_till_done() + assert loaded_entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 9affc1f9db4..83607f1b993 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -4,9 +4,12 @@ from __future__ import annotations from datetime import datetime from unittest.mock import patch -from homeassistant.components.scrape.sensor import DEFAULT_SCAN_INTERVAL +import pytest + +from homeassistant.components.scrape.const import DEFAULT_SCAN_INTERVAL from homeassistant.components.sensor import ( CONF_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorStateClass, ) @@ -21,45 +24,119 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from . import MockRestData, return_config +from . import MockRestData, return_config, return_integration_config -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed DOMAIN = "scrape" async def test_scrape_sensor(hass: HomeAssistant) -> None: """Test Scrape sensor minimal.""" - config = {"sensor": return_config(select=".current-version h1", name="HA version")} + config = { + DOMAIN: [ + return_integration_config( + sensors=[{"select": ".current-version h1", "name": "HA version"}] + ) + ] + } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "Current Version: 2021.12.10" +async def test_scrape_sensor_platform_yaml(hass: HomeAssistant) -> None: + """Test Scrape sensor load from sensor platform.""" + config = { + SENSOR_DOMAIN: [ + return_config( + select=".return", + name="Auth page", + username="user@secret.com", + password="12345678", + authentication="digest", + ), + return_config( + select=".return", + name="Auth page2", + username="user@secret.com", + password="12345678", + template="{{value}}", + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_authentication") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.auth_page") + assert state.state == "secret text" + state2 = hass.states.get("sensor.auth_page2") + assert state2.state == "secret text" + + +async def test_scrape_sensor_platform_yaml_no_data(hass: HomeAssistant, caplog) -> None: + """Test Scrape sensor load from sensor platform fetching no data.""" + config = { + SENSOR_DOMAIN: [ + return_config( + select=".return", + name="Auth page", + username="user@secret.com", + password="12345678", + authentication="digest", + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_no_data") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.auth_page") + assert not state + assert "Platform scrape not ready yet: None; Retrying in background" in caplog.text + + async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: """Test Scrape sensor with value template.""" config = { - "sensor": return_config( - select=".current-version h1", - name="HA version", - template="{{ value.split(':')[1] }}", - ) + DOMAIN: [ + return_integration_config( + sensors=[ + { + "select": ".current-version h1", + "name": "HA version", + "value_template": "{{ value.split(':')[1] }}", + } + ] + ) + ] } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") @@ -69,22 +146,28 @@ async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: """Test Scrape sensor for unit of measurement, device class and state class.""" config = { - "sensor": return_config( - select=".current-temp h3", - name="Current Temp", - template="{{ value.split(':')[1] }}", - uom="°C", - device_class="temperature", - state_class="measurement", - ) + DOMAIN: [ + return_integration_config( + sensors=[ + { + "select": ".current-temp h3", + "name": "Current Temp", + "value_template": "{{ value.split(':')[1] }}", + "unit_of_measurement": "°C", + "device_class": "temperature", + "state_class": "measurement", + } + ] + ) + ] } mocker = MockRestData("test_scrape_uom_and_classes") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.current_temp") @@ -97,20 +180,24 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: async def test_scrape_unique_id(hass: HomeAssistant) -> None: """Test Scrape sensor for unique id.""" config = { - "sensor": return_config( - select=".current-temp h3", - name="Current Temp", - template="{{ value.split(':')[1] }}", - unique_id="very_unique_id", + DOMAIN: return_integration_config( + sensors=[ + { + "select": ".current-temp h3", + "name": "Current Temp", + "value_template": "{{ value.split(':')[1] }}", + "unique_id": "very_unique_id", + } + ] ) } mocker = MockRestData("test_scrape_uom_and_classes") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.current_temp") @@ -125,29 +212,37 @@ async def test_scrape_unique_id(hass: HomeAssistant) -> None: async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: """Test Scrape sensor with authentication.""" config = { - "sensor": [ - return_config( - select=".return", - name="Auth page", - username="user@secret.com", - password="12345678", + DOMAIN: [ + return_integration_config( authentication="digest", - ), - return_config( - select=".return", - name="Auth page2", username="user@secret.com", password="12345678", + sensors=[ + { + "select": ".return", + "name": "Auth page", + }, + ], + ), + return_integration_config( + username="user@secret.com", + password="12345678", + sensors=[ + { + "select": ".return", + "name": "Auth page2", + }, + ], ), ] } mocker = MockRestData("test_scrape_sensor_authentication") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.auth_page") @@ -156,41 +251,55 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: assert state2.state == "secret text" -async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: +async def test_scrape_sensor_no_data( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test Scrape sensor fails on no data.""" - config = {"sensor": return_config(select=".current-version h1", name="HA version")} + config = { + DOMAIN: return_integration_config( + sensors=[{"select": ".current-version h1", "name": "HA version"}] + ) + } mocker = MockRestData("test_scrape_sensor_no_data") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is None + assert "Platform scrape not ready yet" in caplog.text + async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: """Test Scrape sensor no data on refresh.""" - config = {"sensor": return_config(select=".current-version h1", name="HA version")} + config = { + DOMAIN: [ + return_integration_config( + sensors=[{"select": ".current-version h1", "name": "HA version"}] + ) + ] + } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - state = hass.states.get("sensor.ha_version") - assert state - assert state.state == "Current Version: 2021.12.10" + state = hass.states.get("sensor.ha_version") + assert state + assert state.state == "Current Version: 2021.12.10" - mocker.payload = "test_scrape_sensor_no_data" - async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) - await hass.async_block_till_done() + mocker.payload = "test_scrape_sensor_no_data" + async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is not None @@ -200,18 +309,27 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: """Test Scrape sensor with attribute and tag.""" config = { - "sensor": [ - return_config(select="div", name="HA class", index=1, attribute="class"), - return_config(select="template", name="HA template"), + DOMAIN: [ + return_integration_config( + sensors=[ + { + "index": 1, + "select": "div", + "name": "HA class", + "attribute": "class", + }, + {"select": "template", "name": "HA template"}, + ], + ), ] } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_class") @@ -223,21 +341,81 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: async def test_scrape_sensor_errors(hass: HomeAssistant) -> None: """Test Scrape sensor handle errors.""" config = { - "sensor": [ - return_config(select="div", name="HA class", index=5, attribute="class"), - return_config(select="div", name="HA class2", attribute="classes"), + DOMAIN: [ + return_integration_config( + sensors=[ + { + "index": 5, + "select": "div", + "name": "HA class", + "attribute": "class", + }, + { + "select": "div", + "name": "HA class2", + "attribute": "classes", + }, + ], + ), ] } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.rest.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_class") assert state.state == STATE_UNKNOWN state2 = hass.states.get("sensor.ha_class2") assert state2.state == STATE_UNKNOWN + + +async def test_scrape_sensor_unique_id(hass: HomeAssistant) -> None: + """Test Scrape sensor with unique_id.""" + config = { + DOMAIN: [ + return_integration_config( + sensors=[ + { + "select": ".current-version h1", + "name": "HA version", + "unique_id": "ha_version_unique_id", + } + ] + ) + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state.state == "Current Version: 2021.12.10" + + entity_reg = er.async_get(hass) + entity = entity_reg.async_get("sensor.ha_version") + + assert entity.unique_id == "ha_version_unique_id" + + +async def test_setup_config_entry( + hass: HomeAssistant, loaded_entry: MockConfigEntry +) -> None: + """Test setup from config entry.""" + + state = hass.states.get("sensor.current_version") + assert state.state == "Current Version: 2021.12.10" + + entity_reg = er.async_get(hass) + entity = entity_reg.async_get("sensor.current_version") + + assert entity.unique_id == "3699ef88-69e6-11ed-a1eb-0242ac120002" diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 690e5d2e530..7dc2b2469ec 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -22,6 +22,8 @@ MOCK_CONFIG = { "access_token": "ABC", "user_id": "123", "monitor_id": "456", + "device_id": "789", + "refresh_token": "XYZ", } @@ -36,6 +38,8 @@ def mock_sense(): mock_sense.return_value.sense_access_token = "ABC" mock_sense.return_value.sense_user_id = "123" mock_sense.return_value.sense_monitor_id = "456" + mock_sense.return_value.device_id = "789" + mock_sense.return_value.refresh_token = "XYZ" yield mock_sense diff --git a/tests/components/sensirion_ble/__init__.py b/tests/components/sensirion_ble/__init__.py new file mode 100644 index 00000000000..30f89f8934b --- /dev/null +++ b/tests/components/sensirion_ble/__init__.py @@ -0,0 +1 @@ +"""Test package for Sensirion BLE sensor integration.""" diff --git a/tests/components/sensirion_ble/fixtures.py b/tests/components/sensirion_ble/fixtures.py new file mode 100644 index 00000000000..c49ea3c1da2 --- /dev/null +++ b/tests/components/sensirion_ble/fixtures.py @@ -0,0 +1,26 @@ +"""Fixtures for testing Sensirion BLE.""" +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSIRION_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +SENSIRION_SERVICE_INFO = BluetoothServiceInfo( + name="MyCO2", + address="01:03:05:07:09:11", # Ignored (the payload encodes a device ID) + rssi=-60, + manufacturer_data={ + 0x06D5: b"\x00\x08\x84\xe3>_3G\xd4\x02", + }, + service_data={}, + service_uuids=[], + source="local", +) +CONFIGURED_NAME = "MyCO2 84E3" +CONFIGURED_PREFIX = "myco2_84e3" diff --git a/tests/components/sensirion_ble/test_config_flow.py b/tests/components/sensirion_ble/test_config_flow.py new file mode 100644 index 00000000000..9c23a42c90c --- /dev/null +++ b/tests/components/sensirion_ble/test_config_flow.py @@ -0,0 +1,206 @@ +"""Test the Sensirion config flow.""" + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.sensirion_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from .fixtures import ( + CONFIGURED_NAME, + NOT_SENSIRION_SERVICE_INFO, + SENSIRION_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Mock bluetooth for all tests in this module.""" + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSIRION_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address + + +async def test_async_step_bluetooth_not_sensirion(hass): + """Test discovery via bluetooth not sensirion.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSIRION_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.sensirion_ble.config_flow.async_discovered_service_info", + return_value=[SENSIRION_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": SENSIRION_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.sensirion_ble.config_flow.async_discovered_service_info", + return_value=[SENSIRION_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SENSIRION_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": SENSIRION_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SENSIRION_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensirion_ble.config_flow.async_discovered_service_info", + return_value=[SENSIRION_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SENSIRION_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSIRION_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSIRION_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSIRION_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SENSIRION_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.sensirion_ble.config_flow.async_discovered_service_info", + return_value=[SENSIRION_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": SENSIRION_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["data"] == {} + assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address diff --git a/tests/components/sensirion_ble/test_sensor.py b/tests/components/sensirion_ble/test_sensor.py new file mode 100644 index 00000000000..cdec4a0944e --- /dev/null +++ b/tests/components/sensirion_ble/test_sensor.py @@ -0,0 +1,45 @@ +"""Test the Sensirion BLE sensors.""" + +from __future__ import annotations + +from homeassistant.components.sensirion_ble.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant + +from .fixtures import CONFIGURED_NAME, CONFIGURED_PREFIX, SENSIRION_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors(enable_bluetooth, hass: HomeAssistant): + """Test the Sensirion BLE sensors.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=SENSIRION_SERVICE_INFO.address) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info( + hass, + SENSIRION_SERVICE_INFO, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) >= 3 + + for sensor, value, unit, state_class in [ + ("carbon_dioxide", "724", "ppm", "measurement"), + ("humidity", "27.8", "%", "measurement"), + ("temperature", "20.1", "°C", "measurement"), + ]: + state = hass.states.get(f"sensor.{CONFIGURED_PREFIX}_{sensor}") + assert state is not None + assert state.state == value + name_lower = state.attributes[ATTR_FRIENDLY_NAME].lower() + assert name_lower == f"{CONFIGURED_NAME} {sensor}".lower().replace("_", " ") + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit + assert state.attributes[ATTR_STATE_CLASS] == state_class + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index e168a1c2271..c9c29d6c99a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -5,6 +5,7 @@ from decimal import Decimal import pytest from pytest import approx +from homeassistant.components.number import NumberDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -927,3 +928,11 @@ async def test_unit_conversion_priority_legacy_conversion_removed( state = hass.states.get(entity0.entity_id) assert float(state.state) == approx(float(original_value)) assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit + + +def test_device_classes_aligned(): + """Make sure all number device classes are also available in SensorDeviceClass.""" + + for device_class in NumberDeviceClass: + assert hasattr(SensorDeviceClass, device_class.name) + assert getattr(SensorDeviceClass, device_class.name).value == device_class.value diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 05f8bd40597..1e129b7af92 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -14,13 +14,12 @@ from homeassistant.components.recorder.db_schema import StatisticsMeta from homeassistant.components.recorder.models import ( StatisticData, StatisticMetaData, - process_timestamp_to_utc_isoformat, + process_timestamp, ) from homeassistant.components.recorder.statistics import ( async_import_statistics, get_metadata, list_statistic_ids, - statistics_during_period, ) from homeassistant.components.recorder.util import get_instance, session_scope from homeassistant.const import STATE_UNAVAILABLE @@ -32,6 +31,7 @@ from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, do_adhoc_statistics, + statistics_during_period, wait_recording_done, ) @@ -141,6 +141,7 @@ def test_compile_hourly_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -153,9 +154,8 @@ def test_compile_hourly_statistics( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -215,6 +215,7 @@ def test_compile_hourly_statistics_purged_state_changes( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -227,9 +228,8 @@ def test_compile_hourly_statistics_purged_state_changes( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -284,6 +284,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, @@ -292,6 +293,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) "unit_class": "temperature", }, { + "display_unit_of_measurement": "invalid", "has_mean": True, "has_sum": False, "name": None, @@ -301,6 +303,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) "unit_class": None, }, { + "display_unit_of_measurement": None, "has_mean": True, "has_sum": False, "name": None, @@ -311,6 +314,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) }, { "statistic_id": "sensor.test6", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, @@ -320,6 +324,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) }, { "statistic_id": "sensor.test7", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, @@ -332,9 +337,8 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(13.050847), "min": approx(-10.0), "max": approx(30.0), @@ -345,9 +349,8 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) ], "sensor.test2": [ { - "statistic_id": "sensor.test2", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": 13.05084745762712, "min": -10.0, "max": 30.0, @@ -358,9 +361,8 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) ], "sensor.test3": [ { - "statistic_id": "sensor.test3", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": 13.05084745762712, "min": -10.0, "max": 30.0, @@ -371,9 +373,8 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) ], "sensor.test6": [ { - "statistic_id": "sensor.test6", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(13.050847), "min": approx(-10.0), "max": approx(30.0), @@ -384,9 +385,8 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) ], "sensor.test7": [ { - "statistic_id": "sensor.test7", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(13.050847), "min": approx(-10.0), "max": approx(30.0), @@ -480,6 +480,7 @@ async def test_compile_hourly_sum_statistics_amount( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": statistics_unit, "has_mean": False, "has_sum": True, "name": None, @@ -492,35 +493,32 @@ async def test_compile_hourly_sum_statistics_amount( expected_stats = { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(period0), + "last_reset": process_timestamp(period0), "state": approx(factor * seq[2]), "sum": approx(factor * 10.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(factor * seq[5]), "sum": approx(factor * 40.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(factor * seq[8]), "sum": approx(factor * 70.0), }, @@ -672,6 +670,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -684,26 +683,22 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(dt_util.as_local(one)), + "last_reset": process_timestamp(dt_util.as_local(one)), "state": approx(factor * seq[7]), "sum": approx(factor * (sum(seq) - seq[0])), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat( - zero + timedelta(minutes=5) - ), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=10)), + "start": process_timestamp(zero + timedelta(minutes=5)), + "end": process_timestamp(zero + timedelta(minutes=10)), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(dt_util.as_local(two)), + "last_reset": process_timestamp(dt_util.as_local(two)), "state": approx(factor * seq[7]), "sum": approx(factor * (2 * sum(seq) - seq[0])), }, @@ -772,6 +767,7 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -784,13 +780,12 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(dt_util.as_local(one)), + "last_reset": process_timestamp(dt_util.as_local(one)), "state": approx(factor * seq[7]), "sum": approx(factor * (sum(seq) - seq[0] - seq[3])), }, @@ -856,6 +851,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -868,13 +864,12 @@ def test_compile_hourly_sum_statistics_nan_inf_state( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(one), + "last_reset": process_timestamp(one), "state": approx(factor * seq[7]), "sum": approx(factor * (seq[2] + seq[3] + seq[4] + seq[6] + seq[7])), }, @@ -981,6 +976,7 @@ def test_compile_hourly_sum_statistics_negative_state( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert { + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -992,9 +988,8 @@ def test_compile_hourly_sum_statistics_negative_state( stats = statistics_during_period(hass, zero, period="5minute") assert stats[entity_id] == [ { - "statistic_id": entity_id, - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "max": None, "mean": None, "min": None, @@ -1069,6 +1064,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -1081,9 +1077,8 @@ def test_compile_hourly_sum_statistics_total_no_reset( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, @@ -1092,9 +1087,8 @@ def test_compile_hourly_sum_statistics_total_no_reset( "sum": approx(factor * 10.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, @@ -1103,9 +1097,8 @@ def test_compile_hourly_sum_statistics_total_no_reset( "sum": approx(factor * 30.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, @@ -1171,6 +1164,7 @@ def test_compile_hourly_sum_statistics_total_increasing( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -1183,9 +1177,8 @@ def test_compile_hourly_sum_statistics_total_increasing( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, @@ -1194,9 +1187,8 @@ def test_compile_hourly_sum_statistics_total_increasing( "sum": approx(factor * 10.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, @@ -1205,9 +1197,8 @@ def test_compile_hourly_sum_statistics_total_increasing( "sum": approx(factor * 50.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, @@ -1284,6 +1275,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -1297,9 +1289,8 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( "sensor.test1": [ { "last_reset": None, - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, @@ -1308,9 +1299,8 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( }, { "last_reset": None, - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, @@ -1319,9 +1309,8 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( }, { "last_reset": None, - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, @@ -1378,6 +1367,7 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, @@ -1390,35 +1380,32 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(period0), + "last_reset": process_timestamp(period0), "state": approx(20.0), "sum": approx(10.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(40.0), "sum": approx(40.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(70.0), "sum": approx(70.0), }, @@ -1470,6 +1457,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, @@ -1479,6 +1467,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): }, { "statistic_id": "sensor.test2", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, @@ -1488,6 +1477,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): }, { "statistic_id": "sensor.test3", + "display_unit_of_measurement": "Wh", "has_mean": False, "has_sum": True, "name": None, @@ -1500,105 +1490,96 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(period0), + "last_reset": process_timestamp(period0), "state": approx(20.0), "sum": approx(10.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(40.0), "sum": approx(40.0), }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(70.0), "sum": approx(70.0), }, ], "sensor.test2": [ { - "statistic_id": "sensor.test2", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(period0), + "last_reset": process_timestamp(period0), "state": approx(130.0), "sum": approx(20.0), }, { - "statistic_id": "sensor.test2", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(45.0), "sum": approx(-65.0), }, { - "statistic_id": "sensor.test2", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(75.0), "sum": approx(-35.0), }, ], "sensor.test3": [ { - "statistic_id": "sensor.test3", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(period0), + "last_reset": process_timestamp(period0), "state": approx(5.0), "sum": approx(5.0), }, { - "statistic_id": "sensor.test3", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(50.0), "sum": approx(60.0), }, { - "statistic_id": "sensor.test3", - "start": process_timestamp_to_utc_isoformat(period2), - "end": process_timestamp_to_utc_isoformat(period2_end), + "start": process_timestamp(period2), + "end": process_timestamp(period2_end), "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), + "last_reset": process_timestamp(four), "state": approx(90.0), "sum": approx(100.0), }, @@ -1654,9 +1635,8 @@ def test_compile_hourly_statistics_unchanged( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(four), - "end": process_timestamp_to_utc_isoformat(four + timedelta(minutes=5)), + "start": process_timestamp(four), + "end": process_timestamp(four + timedelta(minutes=5)), "mean": approx(value), "min": approx(value), "max": approx(value), @@ -1687,9 +1667,8 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(21.1864406779661), "min": approx(10.0), "max": approx(25.0), @@ -1729,7 +1708,11 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): def test_compile_hourly_statistics_unavailable( hass_recorder, caplog, device_class, state_unit, value ): - """Test compiling hourly statistics, with the sensor being unavailable.""" + """Test compiling hourly statistics, with one sensor being unavailable. + + sensor.test1 is unavailable and should not have statistics generated + sensor.test2 should have statistics generated + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -1753,9 +1736,8 @@ def test_compile_hourly_statistics_unavailable( assert stats == { "sensor.test2": [ { - "statistic_id": "sensor.test2", - "start": process_timestamp_to_utc_isoformat(four), - "end": process_timestamp_to_utc_isoformat(four + timedelta(minutes=5)), + "start": process_timestamp(four), + "end": process_timestamp(four + timedelta(minutes=5)), "mean": approx(value), "min": approx(value), "max": approx(value), @@ -1851,6 +1833,7 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, @@ -1865,6 +1848,7 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, @@ -1958,6 +1942,7 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -1970,9 +1955,8 @@ def test_compile_hourly_statistics_changing_units_1( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -1993,6 +1977,7 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2005,9 +1990,8 @@ def test_compile_hourly_statistics_changing_units_1( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2068,6 +2052,7 @@ def test_compile_hourly_statistics_changing_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "cats", "has_mean": True, "has_sum": False, "name": None, @@ -2133,6 +2118,7 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2145,9 +2131,8 @@ def test_compile_hourly_statistics_changing_units_3( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2168,6 +2153,7 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2180,9 +2166,8 @@ def test_compile_hourly_statistics_changing_units_3( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2247,6 +2232,7 @@ def test_compile_hourly_statistics_equivalent_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2259,9 +2245,8 @@ def test_compile_hourly_statistics_equivalent_units_1( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2278,6 +2263,7 @@ def test_compile_hourly_statistics_equivalent_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit2, "has_mean": True, "has_sum": False, "name": None, @@ -2290,9 +2276,8 @@ def test_compile_hourly_statistics_equivalent_units_1( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2301,11 +2286,8 @@ def test_compile_hourly_statistics_equivalent_units_1( "sum": None, }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat( - zero + timedelta(minutes=10) - ), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "start": process_timestamp(zero + timedelta(minutes=10)), + "end": process_timestamp(zero + timedelta(minutes=15)), "mean": approx(mean2), "min": approx(min), "max": approx(max), @@ -2365,6 +2347,7 @@ def test_compile_hourly_statistics_equivalent_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2377,13 +2360,8 @@ def test_compile_hourly_statistics_equivalent_units_2( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat( - zero + timedelta(seconds=30 * 5) - ), - "end": process_timestamp_to_utc_isoformat( - zero + timedelta(seconds=30 * 15) - ), + "start": process_timestamp(zero + timedelta(seconds=30 * 5)), + "end": process_timestamp(zero + timedelta(seconds=30 * 15)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2435,6 +2413,7 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2447,9 +2426,8 @@ def test_compile_hourly_statistics_changing_device_class_1( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean1), "min": approx(min), "max": approx(max), @@ -2480,6 +2458,7 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2492,9 +2471,8 @@ def test_compile_hourly_statistics_changing_device_class_1( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean1), "min": approx(min), "max": approx(max), @@ -2503,11 +2481,8 @@ def test_compile_hourly_statistics_changing_device_class_1( "sum": None, }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat( - zero + timedelta(minutes=10) - ), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "start": process_timestamp(zero + timedelta(minutes=10)), + "end": process_timestamp(zero + timedelta(minutes=15)), "mean": approx(mean2), "min": approx(min), "max": approx(max), @@ -2538,6 +2513,7 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2550,9 +2526,8 @@ def test_compile_hourly_statistics_changing_device_class_1( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean1), "min": approx(min), "max": approx(max), @@ -2561,11 +2536,8 @@ def test_compile_hourly_statistics_changing_device_class_1( "sum": None, }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat( - zero + timedelta(minutes=10) - ), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "start": process_timestamp(zero + timedelta(minutes=10)), + "end": process_timestamp(zero + timedelta(minutes=15)), "mean": approx(mean2), "min": approx(min), "max": approx(max), @@ -2574,11 +2546,8 @@ def test_compile_hourly_statistics_changing_device_class_1( "sum": None, }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat( - zero + timedelta(minutes=20) - ), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=25)), + "start": process_timestamp(zero + timedelta(minutes=20)), + "end": process_timestamp(zero + timedelta(minutes=25)), "mean": approx(mean2), "min": approx(min), "max": approx(max), @@ -2631,6 +2600,7 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2643,9 +2613,8 @@ def test_compile_hourly_statistics_changing_device_class_2( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2676,6 +2645,7 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2688,9 +2658,8 @@ def test_compile_hourly_statistics_changing_device_class_2( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2699,11 +2668,8 @@ def test_compile_hourly_statistics_changing_device_class_2( "sum": None, }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat( - zero + timedelta(minutes=10) - ), - "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "start": process_timestamp(zero + timedelta(minutes=10)), + "end": process_timestamp(zero + timedelta(minutes=15)), "mean": approx(mean2), "min": approx(min), "max": approx(max), @@ -2758,6 +2724,7 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": None, "has_mean": True, "has_sum": False, "name": None, @@ -2793,6 +2760,7 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": None, "has_mean": False, "has_sum": True, "name": None, @@ -2819,9 +2787,8 @@ def test_compile_hourly_statistics_changing_statistics( assert stats == { "sensor.test1": [ { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period0), - "end": process_timestamp_to_utc_isoformat(period0_end), + "start": process_timestamp(period0), + "end": process_timestamp(period0_end), "mean": approx(mean), "min": approx(min), "max": approx(max), @@ -2830,9 +2797,8 @@ def test_compile_hourly_statistics_changing_statistics( "sum": None, }, { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(period1), - "end": process_timestamp_to_utc_isoformat(period1_end), + "start": process_timestamp(period1), + "end": process_timestamp(period1_end), "mean": None, "min": None, "max": None, @@ -2986,6 +2952,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, @@ -2995,6 +2962,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): }, { "statistic_id": "sensor.test2", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, @@ -3004,6 +2972,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): }, { "statistic_id": "sensor.test3", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, @@ -3013,6 +2982,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): }, { "statistic_id": "sensor.test4", + "display_unit_of_measurement": "EUR", "has_mean": False, "has_sum": True, "name": None, @@ -3067,9 +3037,8 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): ) expected_stats[entity_id].append( { - "statistic_id": entity_id, - "start": process_timestamp_to_utc_isoformat(start), - "end": process_timestamp_to_utc_isoformat(end), + "start": process_timestamp(start), + "end": process_timestamp(end), "mean": approx(expected_average), "min": approx(expected_minimum), "max": approx(expected_maximum), @@ -3125,9 +3094,8 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): ) expected_stats[entity_id].append( { - "statistic_id": entity_id, - "start": process_timestamp_to_utc_isoformat(start), - "end": process_timestamp_to_utc_isoformat(end), + "start": process_timestamp(start), + "end": process_timestamp(end), "mean": approx(expected_average), "min": approx(expected_minimum), "max": approx(expected_maximum), @@ -3183,9 +3151,8 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): ) expected_stats[entity_id].append( { - "statistic_id": entity_id, - "start": process_timestamp_to_utc_isoformat(start), - "end": process_timestamp_to_utc_isoformat(end), + "start": process_timestamp(start), + "end": process_timestamp(end), "mean": approx(expected_average), "min": approx(expected_minimum), "max": approx(expected_maximum), @@ -3241,9 +3208,8 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): ) expected_stats[entity_id].append( { - "statistic_id": entity_id, - "start": process_timestamp_to_utc_isoformat(start), - "end": process_timestamp_to_utc_isoformat(end), + "start": process_timestamp(start), + "end": process_timestamp(end), "mean": approx(expected_average), "min": approx(expected_minimum), "max": approx(expected_maximum), diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index a3c571d7177..b58b147a9a3 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -1,15 +1,39 @@ """Tests for the Shelly integration.""" -from homeassistant.components.shelly.const import CONF_SLEEP_PERIOD, DOMAIN +from __future__ import annotations + +from collections.abc import Mapping +from copy import deepcopy +from datetime import timedelta +from typing import Any +from unittest.mock import Mock + +import pytest + +from homeassistant.components.shelly.const import ( + CONF_SLEEP_PERIOD, + DOMAIN, + REST_SENSORS_UPDATE_INTERVAL, + RPC_SENSORS_POLLING_INTERVAL, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed MOCK_MAC = "123456789ABC" async def init_integration( - hass: HomeAssistant, gen: int, model="SHSW-25", sleep_period=0 + hass: HomeAssistant, + gen: int, + model="SHSW-25", + sleep_period=0, + options: dict[str, Any] | None = None, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the Shelly integration in Home Assistant.""" data = { @@ -19,10 +43,85 @@ async def init_integration( "gen": gen, } - entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC) + entry = MockConfigEntry( + domain=DOMAIN, data=data, unique_id=MOCK_MAC, options=options + ) entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry + + +def mutate_rpc_device_status( + monkeypatch: pytest.MonkeyPatch, + mock_rpc_device: Mock, + top_level_key: str, + key: str, + value: Any, +) -> None: + """Mutate status for rpc device.""" + new_status = deepcopy(mock_rpc_device.status) + new_status[top_level_key][key] = value + monkeypatch.setattr(mock_rpc_device, "status", new_status) + + +def inject_rpc_device_event( + monkeypatch: pytest.MonkeyPatch, + mock_rpc_device: Mock, + event: dict[str, dict[str, Any]], +) -> None: + """Inject event for rpc device.""" + monkeypatch.setattr(mock_rpc_device, "event", event) + mock_rpc_device.mock_event() + + +async def mock_rest_update(hass: HomeAssistant, seconds=REST_SENSORS_UPDATE_INTERVAL): + """Move time to create REST sensors update event.""" + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=seconds)) + await hass.async_block_till_done() + + +async def mock_polling_rpc_update(hass: HomeAssistant): + """Move time to create polling RPC sensors update event.""" + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL) + ) + await hass.async_block_till_done() + + +def register_entity( + hass: HomeAssistant, + domain: str, + object_id: str, + unique_id: str, + config_entry: ConfigEntry | None = None, + capabilities: Mapping[str, Any] | None = None, +) -> str: + """Register enabled entity, return entity_id.""" + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + domain, + DOMAIN, + f"{MOCK_MAC}-{unique_id}", + suggested_object_id=object_id, + disabled_by=None, + config_entry=config_entry, + capabilities=capabilities, + ) + return f"{domain}.{object_id}" + + +def register_device(device_reg, config_entry: ConfigEntry): + """Register Shelly device.""" + device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={ + ( + device_registry.CONNECTION_NETWORK_MAC, + device_registry.format_mac(MOCK_MAC), + ) + }, + ) diff --git a/tests/components/shelly/bluetooth/__init__.py b/tests/components/shelly/bluetooth/__init__.py new file mode 100644 index 00000000000..a4b1f4cdb7e --- /dev/null +++ b/tests/components/shelly/bluetooth/__init__.py @@ -0,0 +1,2 @@ +"""Bluetooth tests for Shelly integration.""" +from __future__ import annotations diff --git a/tests/components/shelly/bluetooth/test_scanner.py b/tests/components/shelly/bluetooth/test_scanner.py new file mode 100644 index 00000000000..160dc557897 --- /dev/null +++ b/tests/components/shelly/bluetooth/test_scanner.py @@ -0,0 +1,146 @@ +"""Test the shelly bluetooth scanner.""" +from __future__ import annotations + +from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT + +from homeassistant.components import bluetooth +from homeassistant.components.shelly.const import CONF_BLE_SCANNER_MODE, BLEScannerMode + +from .. import init_integration, inject_rpc_device_event + + +async def test_scanner(hass, mock_rpc_device, monkeypatch): + """Test injecting data into the scanner.""" + await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + assert mock_rpc_device.initialized is True + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "component": "script:1", + "data": [ + 1, + "aa:bb:cc:dd:ee:ff", + -62, + "AgEGCf9ZANH7O3TIkA==", + "EQcbxdWlAgC4n+YRTSIADaLLBhYADUgQYQ==", + ], + "event": BLE_SCAN_RESULT_EVENT, + "id": 1, + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + ble_device = bluetooth.async_ble_device_from_address( + hass, "AA:BB:CC:DD:EE:FF", connectable=False + ) + assert ble_device is not None + ble_device = bluetooth.async_ble_device_from_address( + hass, "AA:BB:CC:DD:EE:FF", connectable=True + ) + assert ble_device is None + + +async def test_scanner_ignores_non_ble_events(hass, mock_rpc_device, monkeypatch): + """Test injecting non ble data into the scanner.""" + await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + assert mock_rpc_device.initialized is True + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "component": "script:1", + "data": [], + "event": "not_ble_scan_result", + "id": 1, + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + + +async def test_scanner_ignores_wrong_version_and_logs( + hass, mock_rpc_device, monkeypatch, caplog +): + """Test injecting wrong version of ble data into the scanner.""" + await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + assert mock_rpc_device.initialized is True + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "component": "script:1", + "data": [ + 0, + "aa:bb:cc:dd:ee:ff", + -62, + "AgEGCf9ZANH7O3TIkA==", + "EQcbxdWlAgC4n+YRTSIADaLLBhYADUgQYQ==", + ], + "event": BLE_SCAN_RESULT_EVENT, + "id": 1, + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + assert "Unsupported BLE scan result version: 0" in caplog.text + + +async def test_scanner_minimum_firmware_log_error( + hass, mock_rpc_device, monkeypatch, caplog +): + """Test scanner log error if device firmware incompatible.""" + monkeypatch.setattr(mock_rpc_device, "version", "0.11.0") + await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + assert mock_rpc_device.initialized is True + + assert "BLE not supported on device" in caplog.text + + +async def test_scanner_warns_on_corrupt_event( + hass, mock_rpc_device, monkeypatch, caplog +): + """Test injecting garbage ble data into the scanner.""" + await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + assert mock_rpc_device.initialized is True + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "component": "script:1", + "data": [ + 1, + ], + "event": BLE_SCAN_RESULT_EVENT, + "id": 1, + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + assert "Failed to parse BLE event" in caplog.text diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index cd23cc240c5..80e53ac6796 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -1,8 +1,10 @@ """Test configuration for Shelly.""" +from __future__ import annotations + from unittest.mock import AsyncMock, Mock, patch from aioshelly.block_device import BlockDevice -from aioshelly.rpc_device import RpcDevice +from aioshelly.rpc_device import RpcDevice, UpdateType import pytest from homeassistant.components.shelly.const import ( @@ -27,6 +29,8 @@ MOCK_SETTINGS = { "fw": "20201124-092159/v1.9.0@57ac4ad8", "relays": [{"btn_type": "momentary"}, {"btn_type": "toggle"}], "rollers": [{"positioning": True}], + "external_power": 0, + "thermostats": [{"schedule_profile_names": ["Profile1", "Profile2"]}], } @@ -61,9 +65,17 @@ def mock_light_set_state( MOCK_BLOCKS = [ Mock( - sensor_ids={"inputEvent": "S", "inputEventCnt": 2}, + sensor_ids={ + "inputEvent": "S", + "inputEventCnt": 2, + "overpower": 0, + "power": 53.4, + }, channel="0", type="relay", + overpower=0, + power=53.4, + description="relay_0", set_state=AsyncMock(side_effect=lambda turn: {"ison": turn == "on"}), ), Mock( @@ -78,7 +90,7 @@ MOCK_BLOCKS = [ ), ), Mock( - sensor_ids={}, + sensor_ids={"mode": "color", "effect": 0}, channel="0", output=mock_light_set_state()["ison"], colorTemp=mock_light_set_state()["temp"], @@ -86,6 +98,28 @@ MOCK_BLOCKS = [ type="light", set_state=AsyncMock(side_effect=mock_light_set_state), ), + Mock( + sensor_ids={"motion": 0, "temp": 22.1, "gas": "mild"}, + channel="0", + motion=0, + temp=22.1, + gas="mild", + targetTemp=4, + description="sensor_0", + type="sensor", + ), + Mock( + sensor_ids={"battery": 98, "valvePos": 50}, + channel="0", + battery=98, + cfgChanged=0, + mode=0, + valvePos=50, + inputEvent="S", + wakeupEvent=["button"], + description="device_0", + type="device", + ), ] MOCK_CONFIG = { @@ -95,6 +129,7 @@ MOCK_CONFIG = { "sys": { "ui_data": {}, "device": {"name": "Test name"}, + "wakeup_period": 0, }, } @@ -128,18 +163,28 @@ MOCK_STATUS_COAP = { "old_version": "some_old_version", }, "uptime": 5 * REST_SENSORS_UPDATE_INTERVAL, + "wifi_sta": {"rssi": -64}, } MOCK_STATUS_RPC = { "switch:0": {"output": True}, - "cover:0": {"state": "stopped", "pos_control": True, "current_pos": 50}, + "cloud": {"connected": False}, + "cover:0": { + "state": "stopped", + "pos_control": True, + "current_pos": 50, + "apower": 85.3, + }, + "temperature:0": {"tC": 22.9}, "sys": { "available_updates": { "beta": {"version": "some_beta_version"}, "stable": {"version": "some_beta_version"}, } }, + "voltmeter": {"voltage": 4.3}, + "wifi": {"rssi": -63}, } @@ -194,6 +239,7 @@ async def mock_block_device(): blocks=MOCK_BLOCKS, settings=MOCK_SETTINGS, shelly=MOCK_SHELLY_COAP, + version="0.10.0", status=MOCK_STATUS_COAP, firmware_version="some fw string", initialized=True, @@ -204,25 +250,63 @@ async def mock_block_device(): yield block_device_mock.return_value -@pytest.fixture -async def mock_rpc_device(): +def _mock_rpc_device(version: str | None = None): """Mock rpc (Gen2, Websocket) device.""" + return Mock( + spec=RpcDevice, + config=MOCK_CONFIG, + event={}, + shelly=MOCK_SHELLY_RPC, + version=version or "0.12.0", + hostname="test-host", + status=MOCK_STATUS_RPC, + firmware_version="some fw string", + initialized=True, + ) + + +@pytest.fixture +async def mock_pre_ble_rpc_device(): + """Mock rpc (Gen2, Websocket) device pre BLE.""" with patch("aioshelly.rpc_device.RpcDevice.create") as rpc_device_mock: def update(): - rpc_device_mock.return_value.subscribe_updates.call_args[0][0]({}) - - device = Mock( - spec=RpcDevice, - config=MOCK_CONFIG, - event={}, - shelly=MOCK_SHELLY_RPC, - status=MOCK_STATUS_RPC, - firmware_version="some fw string", - initialized=True, - ) + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, UpdateType.STATUS + ) + device = _mock_rpc_device("0.11.0") rpc_device_mock.return_value = device rpc_device_mock.return_value.mock_update = Mock(side_effect=update) yield rpc_device_mock.return_value + + +@pytest.fixture +async def mock_rpc_device(): + """Mock rpc (Gen2, Websocket) device with BLE support.""" + with patch("aioshelly.rpc_device.RpcDevice.create") as rpc_device_mock, patch( + "homeassistant.components.shelly.bluetooth.async_start_scanner" + ): + + def update(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, UpdateType.STATUS + ) + + def event(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, UpdateType.EVENT + ) + + device = _mock_rpc_device("0.12.0") + rpc_device_mock.return_value = device + rpc_device_mock.return_value.mock_update = Mock(side_effect=update) + rpc_device_mock.return_value.mock_event = Mock(side_effect=event) + + yield rpc_device_mock.return_value + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py new file mode 100644 index 00000000000..b39a395d11b --- /dev/null +++ b/tests/components/shelly/test_binary_sensor.py @@ -0,0 +1,214 @@ +"""Tests for Shelly binary sensor platform.""" + + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import State +from homeassistant.helpers.entity_registry import async_get + +from . import ( + init_integration, + mock_rest_update, + mutate_rpc_device_status, + register_device, + register_entity, +) + +from tests.common import mock_restore_cache + +RELAY_BLOCK_ID = 0 +SENSOR_BLOCK_ID = 3 + + +async def test_block_binary_sensor(hass, mock_block_device, monkeypatch): + """Test block binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_channel_1_overpowering" + await init_integration(hass, 1) + + assert hass.states.get(entity_id).state == STATE_OFF + + monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "overpower", 1) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_block_binary_sensor_extra_state_attr( + hass, mock_block_device, monkeypatch +): + """Test block binary sensor extra state attributes.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_gas" + await init_integration(hass, 1) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes.get("detected") == "mild" + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "gas", "none") + mock_block_device.mock_update() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert state.attributes.get("detected") == "none" + + +async def test_block_rest_binary_sensor(hass, mock_block_device, monkeypatch): + """Test block REST binary sensor.""" + entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) + await init_integration(hass, 1) + + assert hass.states.get(entity_id).state == STATE_OFF + + monkeypatch.setitem(mock_block_device.status["cloud"], "connected", True) + await mock_rest_update(hass) + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_block_rest_binary_sensor_connected_battery_devices( + hass, mock_block_device, monkeypatch +): + """Test block REST binary sensor for connected battery devices.""" + entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) + monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHMOS-01") + monkeypatch.setitem(mock_block_device.settings["coiot"], "update_period", 3600) + await init_integration(hass, 1, model="SHMOS-01") + + assert hass.states.get(entity_id).state == STATE_OFF + + monkeypatch.setitem(mock_block_device.status["cloud"], "connected", True) + + # Verify no update on fast intervals + await mock_rest_update(hass) + assert hass.states.get(entity_id).state == STATE_OFF + + # Verify update on slow intervals + await mock_rest_update(hass, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_block_sleeping_binary_sensor(hass, mock_block_device, monkeypatch): + """Test block sleeping binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_motion" + await init_integration(hass, 1, sleep_period=1000) + + # Sensor should be created when device is online + assert hass.states.get(entity_id) is None + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "motion", 1) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_block_restored_sleeping_binary_sensor( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored sleeping binary sensor.""" + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_ON + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + +async def test_rpc_binary_sensor(hass, mock_rpc_device, monkeypatch) -> None: + """Test RPC binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" + await init_integration(hass, 2) + + assert hass.states.get(entity_id).state == STATE_OFF + + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "cover:0", "errors", "overpower" + ) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_rpc_binary_sensor_removal(hass, mock_rpc_device, monkeypatch): + """Test RPC binary sensor is removed due to removal_condition.""" + entity_registry = async_get(hass) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_cover_0_input", "input:0-input" + ) + + assert entity_registry.async_get(entity_id) is not None + + monkeypatch.setattr(mock_rpc_device, "status", {"input:0": {"state": False}}) + await init_integration(hass, 2) + + assert entity_registry.async_get(entity_id) is None + + +async def test_rpc_sleeping_binary_sensor( + hass, mock_rpc_device, device_reg, monkeypatch +) -> None: + """Test RPC online sleeping binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_cloud" + entry = await init_integration(hass, 2, sleep_period=1000) + + # Sensor should be created when device is online + assert hass.states.get(entity_id) is None + + register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry) + + # Make device online + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cloud", "connected", True) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_rpc_restored_sleeping_binary_sensor( + hass, mock_rpc_device, device_reg, monkeypatch +): + """Test RPC restored binary sensor.""" + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + ) + + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_ON + + # Make device online + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index bd20be7c645..2661f55d178 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,4 +1,6 @@ """Tests for Shelly button platform.""" +from __future__ import annotations + from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py new file mode 100644 index 00000000000..0d43ae118cf --- /dev/null +++ b/tests/components/shelly/test_climate.py @@ -0,0 +1,422 @@ +"""Tests for Shelly climate platform.""" +from unittest.mock import AsyncMock, PropertyMock + +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError +import pytest + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, + PRESET_NONE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.components.shelly.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.core import State +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from . import init_integration, register_device, register_entity + +from tests.common import mock_restore_cache + +SENSOR_BLOCK_ID = 3 +DEVICE_BLOCK_ID = 4 +ENTITY_ID = f"{CLIMATE_DOMAIN}.test_name" + + +async def test_climate_hvac_mode(hass, mock_block_device, monkeypatch): + """Test climate hvac mode service.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + # Test initial hvac mode - off + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.OFF + + # Test set hvac mode heat + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": 20.0} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 20.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + + # Test set hvac mode off + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "4"} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.OFF + + # Test unavailable on error + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 1) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + +async def test_climate_set_temperature(hass, mock_block_device, monkeypatch): + """Test climate set temperature service.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.OFF + assert state.attributes[ATTR_TEMPERATURE] == 4 + + # Test set temperature without target temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_TARGET_TEMP_LOW: 20, + ATTR_TARGET_TEMP_HIGH: 30, + }, + blocking=True, + ) + mock_block_device.http_request.assert_not_called() + + # Test set temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 23}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "23.0"} + ) + + +async def test_climate_set_preset_mode(hass, mock_block_device, monkeypatch): + """Test climate set preset mode service.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", None) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # Test set Profile2 + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile2"}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"schedule": 1, "schedule_profile": "2"} + ) + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", 2) + mock_block_device.mock_update() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_PRESET_MODE] == "Profile2" + + # Set preset to none + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + assert len(mock_block_device.http_request.mock_calls) == 2 + mock_block_device.http_request.assert_called_with( + "get", "thermostat/0", {"schedule": 0} + ) + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", 0) + mock_block_device.mock_update() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + +async def test_block_restored_climate(hass, mock_block_device, device_reg, monkeypatch): + """Test block restored climate.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + + # Partial update, should not change state + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.OFF + + +async def test_block_restored_climate_us_customery( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored climate with US CUSTOMATY unit system.""" + hass.config.units = US_CUSTOMARY_SYSTEM + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + attrs = {"current_temperature": 67, "temperature": 68} + mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT, attributes=attrs)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + assert hass.states.get(entity_id).attributes.get("temperature") == 68 + assert hass.states.get(entity_id).attributes.get("current_temperature") == 67 + + # Partial update, should not change state + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + assert hass.states.get(entity_id).attributes.get("temperature") == 68 + assert hass.states.get(entity_id).attributes.get("current_temperature") == 67 + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 19.7) + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + assert hass.states.get(entity_id).attributes.get("temperature") == 67 + assert hass.states.get(entity_id).attributes.get("current_temperature") == 65 + + +async def test_block_restored_climate_unavailable( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored climate unavailable state.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, STATE_UNAVAILABLE)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.OFF + + +async def test_block_restored_climate_set_preset_before_online( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored climate set preset before device is online.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile1"}, + blocking=True, + ) + + mock_block_device.http_request.assert_not_called() + + +async def test_block_set_mode_connection_error(hass, mock_block_device, monkeypatch): + """Test block device set mode connection error.""" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + monkeypatch.setattr( + mock_block_device, + "http_request", + AsyncMock(side_effect=DeviceConnectionError), + ) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + +async def test_block_set_mode_auth_error(hass, mock_block_device, monkeypatch): + """Test block device set mode authentication error.""" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + monkeypatch.setattr( + mock_block_device, + "http_request", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_block_restored_climate_auth_error( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored climate with authentication error during init.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + # Make device online with auth error + monkeypatch.setattr(mock_block_device, "initialized", True) + type(mock_block_device).settings = PropertyMock( + return_value={}, side_effect=InvalidAuthError + ) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index ad28ffbd4f0..1a1acea16a3 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Shelly config flow.""" +from __future__ import annotations + from unittest.mock import AsyncMock, Mock, patch from aioshelly.exceptions import ( @@ -10,8 +12,15 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf -from homeassistant.components.shelly.const import DOMAIN +from homeassistant.components.shelly.const import ( + CONF_BLE_SCANNER_MODE, + DOMAIN, + BLEScannerMode, +) from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.setup import async_setup_component + +from . import init_integration from tests.common import MockConfigEntry @@ -513,18 +522,26 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): assert result3["errors"] == {"base": base_error} -async def test_zeroconf(hass): +@pytest.mark.parametrize( + "gen, get_info", + [ + (1, {"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": 1}), + (2, {"mac": "test-mac", "model": "SHSW-1", "auth": False, "gen": 2}), + ], +) +async def test_zeroconf(hass, gen, get_info): """Test we get the form.""" - with patch( - "aioshelly.common.get_info", - return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, - ), patch( + with patch("aioshelly.common.get_info", return_value=get_info), patch( "aioshelly.block_device.BlockDevice.create", + new=AsyncMock(return_value=Mock(model="SHSW-1", settings=MOCK_SETTINGS)), + ), patch( + "aioshelly.rpc_device.RpcDevice.create", new=AsyncMock( return_value=Mock( - model="SHSW-1", - settings=MOCK_SETTINGS, + shelly={"model": "SHSW-1", "gen": gen}, + config=MOCK_CONFIG, + shutdown=AsyncMock(), ) ), ): @@ -560,7 +577,7 @@ async def test_zeroconf(hass): "host": "1.1.1.1", "model": "SHSW-1", "sleep_period": 0, - "gen": 1, + "gen": gen, } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -880,3 +897,170 @@ async def test_reauth_get_info_error(hass, error): assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" + + +async def test_options_flow_disabled_gen_1(hass, mock_block_device, hass_ws_client): + """Test options are disabled for gen1 devices.""" + await async_setup_component(hass, "config", {}) + entry = await init_integration(hass, 1) + + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + "domain": "shelly", + } + ) + response = await ws_client.receive_json() + assert response["result"][0]["supports_options"] is False + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_options_flow_enabled_gen_2(hass, mock_rpc_device, hass_ws_client): + """Test options are enabled for gen2 devices.""" + await async_setup_component(hass, "config", {}) + entry = await init_integration(hass, 2) + + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + "domain": "shelly", + } + ) + response = await ws_client.receive_json() + assert response["result"][0]["supports_options"] is True + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_options_flow_disabled_sleepy_gen_2( + hass, mock_rpc_device, hass_ws_client +): + """Test options are disabled for sleepy gen2 devices.""" + await async_setup_component(hass, "config", {}) + entry = await init_integration(hass, 2, sleep_period=10) + + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + "domain": "shelly", + } + ) + response = await ws_client.receive_json() + assert response["result"][0]["supports_options"] is False + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_options_flow_ble(hass, mock_rpc_device): + """Test setting ble options for gen2 devices.""" + entry = await init_integration(hass, 2) + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_BLE_SCANNER_MODE: BLEScannerMode.DISABLED, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.DISABLED + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.ACTIVE + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_BLE_SCANNER_MODE: BLEScannerMode.PASSIVE, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.PASSIVE + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_options_flow_pre_ble_device(hass, mock_pre_ble_rpc_device): + """Test setting ble options for gen2 devices with pre ble firmware.""" + entry = await init_integration(hass, 2) + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_BLE_SCANNER_MODE: BLEScannerMode.DISABLED, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.DISABLED + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "ble_unsupported" + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_BLE_SCANNER_MODE: BLEScannerMode.PASSIVE, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "ble_unsupported" + + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py new file mode 100644 index 00000000000..eab5c21113a --- /dev/null +++ b/tests/components/shelly/test_coordinator.py @@ -0,0 +1,531 @@ +"""Tests for Shelly coordinator.""" +from datetime import timedelta +from unittest.mock import AsyncMock + +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.shelly.const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, + ATTR_GENERATION, + DOMAIN, + ENTRY_RELOAD_COOLDOWN, + RPC_RECONNECT_INTERVAL, + SLEEP_PERIOD_MULTIPLIER, + UPDATE_PERIOD_MULTIPLIER, +) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) +from homeassistant.util import dt + +from . import ( + init_integration, + inject_rpc_device_event, + mock_polling_rpc_update, + mock_rest_update, + register_entity, +) + +from tests.common import async_fire_time_changed + +RELAY_BLOCK_ID = 0 +LIGHT_BLOCK_ID = 2 +SENSOR_BLOCK_ID = 3 +DEVICE_BLOCK_ID = 4 + + +async def test_block_reload_on_cfg_change(hass, mock_block_device, monkeypatch): + """Test block reload on config change.""" + await init_integration(hass, 1) + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1) + mock_block_device.mock_update() + await hass.async_block_till_done() + + # Generate config change from switch to light + monkeypatch.setitem( + mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" + ) + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 2) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + + # Wait for debouncer + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is None + + +async def test_block_no_reload_on_bulb_changes(hass, mock_block_device, monkeypatch): + """Test block no reload on bulb mode/effect change.""" + await init_integration(hass, 1, model="SHBLB-1") + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1) + mock_block_device.mock_update() + await hass.async_block_till_done() + + # Test no reload on mode change + monkeypatch.setitem( + mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" + ) + monkeypatch.setattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode", "white") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 2) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + + # Wait for debouncer + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + + # Test no reload on effect change + monkeypatch.setattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "effect", 1) + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 3) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + + # Wait for debouncer + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + + +async def test_block_polling_auth_error(hass, mock_block_device, monkeypatch): + """Test block device polling authentication error.""" + monkeypatch.setattr( + mock_block_device, + "update", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 1) + + assert entry.state == ConfigEntryState.LOADED + + # Move time to generate polling + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15) + ) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_block_rest_update_auth_error(hass, mock_block_device, monkeypatch): + """Test block REST update authentication error.""" + register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) + monkeypatch.setitem(mock_block_device.status, "uptime", 1) + entry = await init_integration(hass, 1) + + monkeypatch.setattr( + mock_block_device, + "update_shelly", + AsyncMock(side_effect=InvalidAuthError), + ) + + assert entry.state == ConfigEntryState.LOADED + + await mock_rest_update(hass) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_block_polling_connection_error(hass, mock_block_device, monkeypatch): + """Test block device polling connection error.""" + monkeypatch.setattr( + mock_block_device, + "update", + AsyncMock(side_effect=DeviceConnectionError), + ) + await init_integration(hass, 1) + + assert hass.states.get("switch.test_name_channel_1").state == STATE_ON + + # Move time to generate polling + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15) + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1").state == STATE_UNAVAILABLE + + +async def test_block_rest_update_connection_error(hass, mock_block_device, monkeypatch): + """Test block REST update connection error.""" + entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": True}) + monkeypatch.setitem(mock_block_device.status, "uptime", 1) + await init_integration(hass, 1) + + await mock_rest_update(hass) + assert hass.states.get(entity_id).state == STATE_ON + + monkeypatch.setattr( + mock_block_device, + "update_shelly", + AsyncMock(side_effect=DeviceConnectionError), + ) + await mock_rest_update(hass) + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_block_sleeping_device_no_periodic_updates(hass, mock_block_device): + """Test block sleeping device no periodic updates.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "22.1" + + # Move time to generate polling + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000) + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_block_button_click_event(hass, mock_block_device, events, monkeypatch): + """Test block click event for Shelly button.""" + monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "sensor_ids", {}) + monkeypatch.setattr( + mock_block_device.blocks[DEVICE_BLOCK_ID], + "sensor_ids", + {"inputEvent": "S", "inputEventCnt": 0}, + ) + entry = await init_integration(hass, 1, model="SHBTN-1", sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + + # Generate button click event + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + ATTR_DEVICE_ID: device.id, + ATTR_DEVICE: "test-host", + ATTR_CHANNEL: 1, + ATTR_CLICK_TYPE: "single", + ATTR_GENERATION: 1, + } + + # Test ignore empty event + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "inputEvent", "") + mock_block_device.mock_update() + await hass.async_block_till_done() + + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert len(events) == 1 + + +async def test_rpc_reload_on_cfg_change(hass, mock_rpc_device, monkeypatch): + """Test RPC reload on config change.""" + await init_integration(hass, 2) + + # Generate config change from switch to light + monkeypatch.setitem( + mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] + ) + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "data": [], + "event": "config_changed", + "id": 1, + "ts": 1668522399.2, + }, + { + "data": [], + "id": 2, + "ts": 1668522399.2, + }, + ], + "ts": 1668522399.2, + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_switch_0") is not None + + # Wait for debouncer + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_switch_0") is None + + +async def test_rpc_click_event(hass, mock_rpc_device, events, monkeypatch): + """Test RPC click event.""" + entry = await init_integration(hass, 2) + + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + + # Generate config change from switch to light + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "data": [], + "event": "single_push", + "id": 0, + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + ATTR_DEVICE_ID: device.id, + ATTR_DEVICE: "test-host", + ATTR_CHANNEL: 1, + ATTR_CLICK_TYPE: "single_push", + ATTR_GENERATION: 2, + } + + +async def test_rpc_update_entry_sleep_period(hass, mock_rpc_device, monkeypatch): + """Test RPC update entry sleep period.""" + entry = await init_integration(hass, 2, sleep_period=600) + register_entity( + hass, + SENSOR_DOMAIN, + "test_name_temperature", + "temperature:0-temperature_0", + entry, + ) + + # Make device online + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert entry.data["sleep_period"] == 600 + + # Move time to generate sleep period update + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER) + ) + await hass.async_block_till_done() + + assert entry.data["sleep_period"] == 3600 + + +async def test_rpc_sleeping_device_no_periodic_updates( + hass, mock_rpc_device, monkeypatch +): + """Test RPC sleeping device no periodic updates.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + entry = await init_integration(hass, 2, sleep_period=1000) + register_entity( + hass, + SENSOR_DOMAIN, + "test_name_temperature", + "temperature:0-temperature_0", + entry, + ) + + # Make device online + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "22.9" + + # Move time to generate polling + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000) + ) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_rpc_reconnect_auth_error(hass, mock_rpc_device, monkeypatch): + """Test RPC reconnect authentication error.""" + entry = await init_integration(hass, 2) + + monkeypatch.setattr(mock_rpc_device, "connected", False) + monkeypatch.setattr( + mock_rpc_device, + "initialize", + AsyncMock( + side_effect=InvalidAuthError, + ), + ) + + assert entry.state == ConfigEntryState.LOADED + + # Move time to generate reconnect + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=RPC_RECONNECT_INTERVAL) + ) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_rpc_polling_auth_error(hass, mock_rpc_device, monkeypatch) -> None: + """Test RPC polling authentication error.""" + register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") + entry = await init_integration(hass, 2) + + monkeypatch.setattr( + mock_rpc_device, + "update_status", + AsyncMock( + side_effect=InvalidAuthError, + ), + ) + + assert entry.state == ConfigEntryState.LOADED + + await mock_polling_rpc_update(hass) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_rpc_reconnect_error(hass, mock_rpc_device, monkeypatch): + """Test RPC reconnect error.""" + await init_integration(hass, 2) + + assert hass.states.get("switch.test_switch_0").state == STATE_ON + + monkeypatch.setattr(mock_rpc_device, "connected", False) + monkeypatch.setattr( + mock_rpc_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + + # Move time to generate reconnect + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=RPC_RECONNECT_INTERVAL) + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_switch_0").state == STATE_UNAVAILABLE + + +async def test_rpc_polling_connection_error(hass, mock_rpc_device, monkeypatch) -> None: + """Test RPC polling connection error.""" + entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") + await init_integration(hass, 2) + + monkeypatch.setattr( + mock_rpc_device, + "update_status", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + + assert hass.states.get(entity_id).state == "-63" + + await mock_polling_rpc_update(hass) + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_rpc_polling_disconnected(hass, mock_rpc_device, monkeypatch) -> None: + """Test RPC polling device disconnected.""" + entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") + await init_integration(hass, 2) + + monkeypatch.setattr(mock_rpc_device, "connected", False) + + assert hass.states.get(entity_id).state == "-63" + + await mock_polling_rpc_update(hass) + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 51fef7dc030..34f63b7690c 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -1,4 +1,8 @@ """Tests for Shelly cover platform.""" +from unittest.mock import Mock + +import pytest + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, @@ -13,8 +17,9 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant -from . import init_integration +from . import init_integration, mutate_rpc_device_status ROLLER_BLOCK_ID = 1 @@ -77,7 +82,9 @@ async def test_block_device_no_roller_blocks(hass, mock_block_device, monkeypatc assert hass.states.get("cover.test_name") is None -async def test_rpc_device_services(hass, mock_rpc_device, monkeypatch): +async def test_rpc_device_services( + hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch +) -> None: """Test RPC device cover services.""" await init_integration(hass, 2) @@ -90,7 +97,9 @@ async def test_rpc_device_services(hass, mock_rpc_device, monkeypatch): state = hass.states.get("cover.test_cover_0") assert state.attributes[ATTR_CURRENT_POSITION] == 50 - monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "opening") + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "cover:0", "state", "opening" + ) await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, @@ -100,7 +109,9 @@ async def test_rpc_device_services(hass, mock_rpc_device, monkeypatch): mock_rpc_device.mock_update() assert hass.states.get("cover.test_cover_0").state == STATE_OPENING - monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "closing") + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "cover:0", "state", "closing" + ) await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, @@ -110,7 +121,7 @@ async def test_rpc_device_services(hass, mock_rpc_device, monkeypatch): mock_rpc_device.mock_update() assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING - monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "closed") + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, @@ -121,26 +132,34 @@ async def test_rpc_device_services(hass, mock_rpc_device, monkeypatch): assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED -async def test_rpc_device_no_cover_keys(hass, mock_rpc_device, monkeypatch): +async def test_rpc_device_no_cover_keys( + hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch +) -> None: """Test RPC device without cover keys.""" monkeypatch.delitem(mock_rpc_device.status, "cover:0") await init_integration(hass, 2) assert hass.states.get("cover.test_cover_0") is None -async def test_rpc_device_update(hass, mock_rpc_device, monkeypatch): +async def test_rpc_device_update( + hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch +) -> None: """Test RPC device update.""" - monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "closed") + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await init_integration(hass, 2) assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED - monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "open") + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open") mock_rpc_device.mock_update() assert hass.states.get("cover.test_cover_0").state == STATE_OPEN -async def test_rpc_device_no_position_control(hass, mock_rpc_device, monkeypatch): +async def test_rpc_device_no_position_control( + hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch +) -> None: """Test RPC device with no position control.""" - monkeypatch.setitem(mock_rpc_device.status["cover:0"], "pos_control", False) + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "cover:0", "pos_control", False + ) await init_integration(hass, 2) assert hass.states.get("cover.test_cover_0").state == STATE_OPEN diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index d5881696bf6..ec74745da15 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -240,9 +240,14 @@ async def test_if_fires_on_click_event_rpc_device(hass, calls, mock_rpc_device): assert calls[0].data["some"] == "test_trigger_single_push" -async def test_validate_trigger_block_device_not_ready(hass, calls, mock_block_device): +async def test_validate_trigger_block_device_not_ready( + hass, calls, mock_block_device, monkeypatch +): """Test validate trigger config when block device is not ready.""" - await init_integration(hass, 1) + monkeypatch.setattr(mock_block_device, "initialized", False) + entry = await init_integration(hass, 1) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] assert await async_setup_component( hass, @@ -253,7 +258,7 @@ async def test_validate_trigger_block_device_not_ready(hass, calls, mock_block_d "trigger": { CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, - CONF_DEVICE_ID: "device_not_ready", + CONF_DEVICE_ID: device.id, CONF_TYPE: "single", CONF_SUBTYPE: "button1", }, @@ -266,7 +271,7 @@ async def test_validate_trigger_block_device_not_ready(hass, calls, mock_block_d }, ) message = { - CONF_DEVICE_ID: "device_not_ready", + CONF_DEVICE_ID: device.id, ATTR_CLICK_TYPE: "single", ATTR_CHANNEL: 1, } @@ -277,8 +282,15 @@ async def test_validate_trigger_block_device_not_ready(hass, calls, mock_block_d assert calls[0].data["some"] == "test_trigger_single_click" -async def test_validate_trigger_rpc_device_not_ready(hass, calls, mock_rpc_device): +async def test_validate_trigger_rpc_device_not_ready( + hass, calls, mock_rpc_device, monkeypatch +): """Test validate trigger config when RPC device is not ready.""" + monkeypatch.setattr(mock_rpc_device, "initialized", False) + entry = await init_integration(hass, 2) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + assert await async_setup_component( hass, automation.DOMAIN, @@ -288,7 +300,7 @@ async def test_validate_trigger_rpc_device_not_ready(hass, calls, mock_rpc_devic "trigger": { CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, - CONF_DEVICE_ID: "device_not_ready", + CONF_DEVICE_ID: device.id, CONF_TYPE: "single_push", CONF_SUBTYPE: "button1", }, @@ -301,7 +313,7 @@ async def test_validate_trigger_rpc_device_not_ready(hass, calls, mock_rpc_devic }, ) message = { - CONF_DEVICE_ID: "device_not_ready", + CONF_DEVICE_ID: device.id, ATTR_CLICK_TYPE: "single_push", ATTR_CHANNEL: 1, } diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index a99b28d48e0..ccac9bcc1b0 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -70,6 +70,7 @@ async def test_rpc_config_entry_diagnostics( "beta": {"version": "some_beta_version"}, "stable": {"version": "some_beta_version"}, } - } + }, + "wifi": {"rssi": -63}, }, } diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index f795b79132f..48ea978e5d9 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -1,11 +1,16 @@ """Test cases for the Shelly component.""" +from __future__ import annotations -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError import pytest -from homeassistant.components.shelly.const import DOMAIN +from homeassistant.components.shelly.const import ( + CONF_BLE_SCANNER_MODE, + DOMAIN, + BLEScannerMode, +) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.helpers import device_registry @@ -182,3 +187,27 @@ async def test_entry_unload_device_not_ready( await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_entry_unload_not_connected(hass, mock_rpc_device, monkeypatch): + """Test entry unload when not connected.""" + with patch( + "homeassistant.components.shelly.coordinator.async_stop_scanner" + ) as mock_stop_scanner: + + entry = await init_integration( + hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} + ) + entity_id = "switch.test_switch_0" + + assert entry.state is ConfigEntryState.LOADED + assert hass.states.get(entity_id).state is STATE_ON + assert not mock_stop_scanner.call_count + + monkeypatch.setattr(mock_rpc_device, "connected", False) + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert not mock_stop_scanner.call_count + assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index b0162f43e13..5f8d49fa8aa 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -25,7 +25,7 @@ from homeassistant.const import ( STATE_ON, ) -from . import init_integration +from . import init_integration, mutate_rpc_device_status RELAY_BLOCK_ID = 0 LIGHT_BLOCK_ID = 2 @@ -374,7 +374,7 @@ async def test_rpc_device_switch_type_lights_mode(hass, mock_rpc_device, monkeyp ) assert hass.states.get("light.test_switch_0").state == STATE_ON - monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False) + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "switch:0", "output", False) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py new file mode 100644 index 00000000000..69b1105fef5 --- /dev/null +++ b/tests/components/shelly/test_number.py @@ -0,0 +1,152 @@ +"""Tests for Shelly number platform.""" +from unittest.mock import AsyncMock + +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError +import pytest + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.components.shelly.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import State +from homeassistant.exceptions import HomeAssistantError + +from . import init_integration, register_device, register_entity + +from tests.common import mock_restore_cache + +DEVICE_BLOCK_ID = 4 + + +async def test_block_number_update(hass, mock_block_device, monkeypatch): + """Test block device number update.""" + await init_integration(hass, 1, sleep_period=1000) + + assert hass.states.get("number.test_name_valve_position") is None + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get("number.test_name_valve_position").state == "50" + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valvePos", 30) + mock_block_device.mock_update() + + assert hass.states.get("number.test_name_valve_position").state == "30" + + +async def test_block_restored_number(hass, mock_block_device, device_reg, monkeypatch): + """Test block restored number.""" + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + capabilities = { + "min": 0, + "max": 100, + "step": 1, + "mode": "slider", + } + entity_id = register_entity( + hass, + NUMBER_DOMAIN, + "test_name_valve_position", + "device_0-valvePos", + entry, + capabilities, + ) + mock_restore_cache(hass, [State(entity_id, "40")]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "40" + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "50" + + +async def test_block_number_set_value(hass, mock_block_device): + """Test block device number set value.""" + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + mock_block_device.reset_mock() + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_name_valve_position", ATTR_VALUE: 30}, + blocking=True, + ) + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"pos": 30.0} + ) + + +async def test_block_set_value_connection_error(hass, mock_block_device, monkeypatch): + """Test block device set value connection error.""" + monkeypatch.setattr( + mock_block_device, + "http_request", + AsyncMock(side_effect=DeviceConnectionError), + ) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_name_valve_position", ATTR_VALUE: 30}, + blocking=True, + ) + + +async def test_block_set_value_auth_error(hass, mock_block_device, monkeypatch): + """Test block device set value authentication error.""" + monkeypatch.setattr( + mock_block_device, + "http_request", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_name_valve_position", ATTR_VALUE: 30}, + blocking=True, + ) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py new file mode 100644 index 00000000000..6dcd903219f --- /dev/null +++ b/tests/components/shelly/test_sensor.py @@ -0,0 +1,267 @@ +"""Tests for Shelly sensor platform.""" + + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import State +from homeassistant.helpers.entity_registry import async_get + +from . import ( + init_integration, + mock_polling_rpc_update, + mock_rest_update, + mutate_rpc_device_status, + register_device, + register_entity, +) + +from tests.common import mock_restore_cache + +RELAY_BLOCK_ID = 0 +SENSOR_BLOCK_ID = 3 +DEVICE_BLOCK_ID = 4 + + +async def test_block_sensor(hass, mock_block_device, monkeypatch): + """Test block sensor.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_power" + await init_integration(hass, 1) + + assert hass.states.get(entity_id).state == "53.4" + + monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "power", 60.1) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == "60.1" + + +async def test_block_rest_sensor(hass, mock_block_device, monkeypatch): + """Test block REST sensor.""" + entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "rssi") + await init_integration(hass, 1) + + assert hass.states.get(entity_id).state == "-64" + + monkeypatch.setitem(mock_block_device.status["wifi_sta"], "rssi", -71) + await mock_rest_update(hass) + + assert hass.states.get(entity_id).state == "-71" + + +async def test_block_sleeping_sensor(hass, mock_block_device, monkeypatch): + """Test block sleeping sensor.""" + monkeypatch.setattr( + mock_block_device.blocks[DEVICE_BLOCK_ID], "sensor_ids", {"battery": 98} + ) + entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + await init_integration(hass, 1, sleep_period=1000) + + # Sensor should be created when device is online + assert hass.states.get(entity_id) is None + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "22.1" + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 23.4) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == "23.4" + + +async def test_block_restored_sleeping_sensor( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored sleeping sensor.""" + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry + ) + mock_restore_cache(hass, [State(entity_id, "20.4")]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "20.4" + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "22.1" + + +async def test_block_sensor_error(hass, mock_block_device, monkeypatch): + """Test block sensor unavailable on sensor error.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_battery" + await init_integration(hass, 1) + + assert hass.states.get(entity_id).state == "98" + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "battery", -1) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_block_sensor_removal(hass, mock_block_device, monkeypatch): + """Test block sensor is removed due to removal_condition.""" + entity_registry = async_get(hass) + entity_id = register_entity( + hass, SENSOR_DOMAIN, "test_name_battery", "device_0-battery" + ) + + assert entity_registry.async_get(entity_id) is not None + + monkeypatch.setitem(mock_block_device.settings, "external_power", 1) + await init_integration(hass, 1) + + assert entity_registry.async_get(entity_id) is None + + +async def test_block_not_matched_restored_sleeping_sensor( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block not matched to restored sleeping sensor.""" + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry + ) + mock_restore_cache(hass, [State(entity_id, "20.4")]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "20.4" + + # Make device online + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "type", "other_type") + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "20.4" + + +async def test_block_sensor_without_value(hass, mock_block_device, monkeypatch): + """Test block sensor without value is not created.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_battery" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "battery", None) + await init_integration(hass, 1) + + assert hass.states.get(entity_id) is None + + +async def test_block_sensor_unknown_value(hass, mock_block_device, monkeypatch): + """Test block sensor unknown value.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_battery" + await init_integration(hass, 1) + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "battery", None) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + +async def test_rpc_sensor(hass, mock_rpc_device, monkeypatch) -> None: + """Test RPC sensor.""" + entity_id = f"{SENSOR_DOMAIN}.test_cover_0_power" + await init_integration(hass, 2) + + assert hass.states.get(entity_id).state == "85.3" + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "apower", "88.2") + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == "88.2" + + +async def test_rpc_sensor_error(hass, mock_rpc_device, monkeypatch): + """Test RPC sensor unavailable on sensor error.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_voltmeter" + await init_integration(hass, 2) + + assert hass.states.get(entity_id).state == "4.3" + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "voltmeter", "voltage", None) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_rpc_polling_sensor(hass, mock_rpc_device, monkeypatch) -> None: + """Test RPC polling sensor.""" + entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") + await init_integration(hass, 2) + + assert hass.states.get(entity_id).state == "-63" + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "wifi", "rssi", "-70") + await mock_polling_rpc_update(hass) + + assert hass.states.get(entity_id).state == "-70" + + +async def test_rpc_sleeping_sensor( + hass, mock_rpc_device, device_reg, monkeypatch +) -> None: + """Test RPC online sleeping sensor.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + entry = await init_integration(hass, 2, sleep_period=1000) + + # Sensor should be created when device is online + assert hass.states.get(entity_id) is None + + register_entity( + hass, + SENSOR_DOMAIN, + "test_name_temperature", + "temperature:0-temperature_0", + entry, + ) + + # Make device online + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "22.9" + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "temperature:0", "tC", 23.4) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == "23.4" + + +async def test_rpc_restored_sleeping_sensor( + hass, mock_rpc_device, device_reg, monkeypatch +): + """Test RPC restored sensor.""" + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + SENSOR_DOMAIN, + "test_name_temperature", + "temperature:0-temperature_0", + entry, + ) + + mock_restore_cache(hass, [State(entity_id, "21.0")]) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "21.0" + + # Make device online + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "22.9" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 458de9c655b..a5e7a56065d 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -1,5 +1,12 @@ """Tests for Shelly switch platform.""" +from unittest.mock import AsyncMock + +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError +import pytest + +from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -7,6 +14,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.exceptions import HomeAssistantError from . import init_integration @@ -34,6 +42,56 @@ async def test_block_device_services(hass, mock_block_device): assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF +async def test_block_set_state_connection_error(hass, mock_block_device, monkeypatch): + """Test block device set state connection error.""" + monkeypatch.setattr( + mock_block_device.blocks[RELAY_BLOCK_ID], + "set_state", + AsyncMock(side_effect=DeviceConnectionError), + ) + await init_integration(hass, 1) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_name_channel_1"}, + blocking=True, + ) + + +async def test_block_set_state_auth_error(hass, mock_block_device, monkeypatch): + """Test block device set state authentication error.""" + monkeypatch.setattr( + mock_block_device.blocks[RELAY_BLOCK_ID], + "set_state", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 1) + + assert entry.state == ConfigEntryState.LOADED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_name_channel_1"}, + blocking=True, + ) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + async def test_block_device_update(hass, mock_block_device, monkeypatch): """Test block device update.""" monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "output", False) @@ -98,3 +156,50 @@ async def test_rpc_device_switch_type_lights_mode(hass, mock_rpc_device, monkeyp ) await init_integration(hass, 2) assert hass.states.get("switch.test_switch_0") is None + + +@pytest.mark.parametrize("exc", [DeviceConnectionError, RpcCallError(-1, "error")]) +async def test_rpc_set_state_errors(hass, exc, mock_rpc_device, monkeypatch): + """Test RPC device set state connection/call errors.""" + monkeypatch.setattr(mock_rpc_device, "call_rpc", AsyncMock(side_effect=exc)) + await init_integration(hass, 2) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_switch_0"}, + blocking=True, + ) + + +async def test_rpc_auth_error(hass, mock_rpc_device, monkeypatch): + """Test RPC device set state authentication error.""" + monkeypatch.setattr( + mock_rpc_device, + "call_rpc", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 2) + + assert entry.state == ConfigEntryState.LOADED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_switch_0"}, + blocking=True, + ) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 4da81e076ae..fe7d979e797 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -1,5 +1,4 @@ """Tests for Shelly update platform.""" -from datetime import timedelta from unittest.mock import AsyncMock from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError @@ -7,7 +6,7 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.shelly.const import DOMAIN, REST_SENSORS_UPDATE_INTERVAL +from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, @@ -20,11 +19,8 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import dt -from . import MOCK_MAC, init_integration - -from tests.common import async_fire_time_changed +from . import MOCK_MAC, init_integration, mock_rest_update @pytest.mark.parametrize( @@ -100,10 +96,7 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch) assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_firmware_update") assert state.state == STATE_OFF @@ -134,10 +127,7 @@ async def test_block_beta_update(hass: HomeAssistant, mock_block_device, monkeyp assert state.attributes[ATTR_IN_PROGRESS] is False monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "2b") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_ON @@ -160,10 +150,7 @@ async def test_block_beta_update(hass: HomeAssistant, mock_block_device, monkeyp assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2b") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_OFF @@ -288,10 +275,7 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_firmware_update") assert state.state == STATE_OFF @@ -335,10 +319,7 @@ async def test_rpc_beta_update(hass: HomeAssistant, mock_rpc_device, monkeypatch "beta": {"version": "2b"}, }, ) - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_ON @@ -361,10 +342,7 @@ async def test_rpc_beta_update(hass: HomeAssistant, mock_rpc_device, monkeypatch assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2b") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_OFF diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py new file mode 100644 index 00000000000..c817b7d620c --- /dev/null +++ b/tests/components/shelly/test_utils.py @@ -0,0 +1,226 @@ +"""Tests for Shelly utils.""" +from freezegun import freeze_time +import pytest + +from homeassistant.components.shelly.utils import ( + get_block_channel_name, + get_block_device_sleep_period, + get_block_input_triggers, + get_device_uptime, + get_number_of_channels, + get_rpc_channel_name, + get_rpc_input_triggers, + is_block_momentary_input, +) +from homeassistant.util import dt + +DEVICE_BLOCK_ID = 4 + + +async def test_block_get_number_of_channels(mock_block_device, monkeypatch): + """Test block get number of channels.""" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "emeter") + monkeypatch.setitem(mock_block_device.shelly, "num_emeters", 3) + + assert ( + get_number_of_channels( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + == 3 + ) + + monkeypatch.setitem(mock_block_device.shelly, "num_inputs", 4) + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "input") + assert ( + get_number_of_channels( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + == 4 + ) + + monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHDM-2") + assert ( + get_number_of_channels( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + == 2 + ) + + +async def test_block_get_block_channel_name(mock_block_device, monkeypatch): + """Test block get block channel name.""" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "relay") + + assert ( + get_block_channel_name( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + == "Test name channel 1" + ) + + monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHEM-3") + + assert ( + get_block_channel_name( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + == "Test name channel A" + ) + + monkeypatch.setitem( + mock_block_device.settings, "relays", [{"name": "test-channel"}] + ) + + assert ( + get_block_channel_name( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + == "test-channel" + ) + + +async def test_is_block_momentary_input(mock_block_device, monkeypatch): + """Test is block momentary input.""" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "relay") + + monkeypatch.setitem(mock_block_device.settings, "mode", "roller") + monkeypatch.setitem( + mock_block_device.settings, "rollers", [{"button_type": "detached"}] + ) + assert ( + is_block_momentary_input( + mock_block_device.settings, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + is False + ) + assert ( + is_block_momentary_input( + mock_block_device.settings, mock_block_device.blocks[DEVICE_BLOCK_ID], True + ) + is True + ) + + monkeypatch.setitem(mock_block_device.settings, "mode", "relay") + monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHSW-L") + assert ( + is_block_momentary_input( + mock_block_device.settings, mock_block_device.blocks[DEVICE_BLOCK_ID], True + ) + is False + ) + + monkeypatch.delitem(mock_block_device.settings, "relays") + monkeypatch.delitem(mock_block_device.settings, "rollers") + assert ( + is_block_momentary_input( + mock_block_device.settings, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + is False + ) + + monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHBTN-2") + + assert ( + is_block_momentary_input( + mock_block_device.settings, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + is True + ) + + +@pytest.mark.parametrize( + "settings, sleep_period", + [ + ({}, 0), + ({"sleep_mode": {"period": 1000, "unit": "m"}}, 1000 * 60), + ({"sleep_mode": {"period": 5, "unit": "h"}}, 5 * 3600), + ], +) +async def test_get_block_device_sleep_period(settings, sleep_period): + """Test get block device sleep period.""" + assert get_block_device_sleep_period(settings) == sleep_period + + +@freeze_time("2019-01-10 18:43:00+00:00") +async def test_get_device_uptime(): + """Test block test get device uptime.""" + assert get_device_uptime( + 55, dt.as_utc(dt.parse_datetime("2019-01-10 18:42:00+00:00")) + ) == dt.as_utc(dt.parse_datetime("2019-01-10 18:42:00+00:00")) + + assert get_device_uptime( + 50, dt.as_utc(dt.parse_datetime("2019-01-10 18:42:00+00:00")) + ) == dt.as_utc(dt.parse_datetime("2019-01-10 18:42:10+00:00")) + + +async def test_get_block_input_triggers(mock_block_device, monkeypatch): + """Test get block input triggers.""" + monkeypatch.setattr( + mock_block_device.blocks[DEVICE_BLOCK_ID], + "sensor_ids", + {"inputEvent": "S", "inputEventCnt": 0}, + ) + monkeypatch.setitem( + mock_block_device.settings, "rollers", [{"button_type": "detached"}] + ) + assert set( + get_block_input_triggers( + mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID] + ) + ) == {("long", "button"), ("single", "button")} + + monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHBTN-1") + assert set( + get_block_input_triggers( + mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID] + ) + ) == { + ("long", "button"), + ("double", "button"), + ("single", "button"), + ("triple", "button"), + } + + monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHIX3-1") + assert set( + get_block_input_triggers( + mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID] + ) + ) == { + ("long_single", "button"), + ("single_long", "button"), + ("triple", "button"), + ("long", "button"), + ("single", "button"), + ("double", "button"), + } + + +async def test_get_rpc_channel_name(mock_rpc_device): + """Test get RPC channel name.""" + assert get_rpc_channel_name(mock_rpc_device, "input:0") == "test switch_0" + assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Test name switch_3" + + +async def test_get_rpc_input_triggers(mock_rpc_device, monkeypatch): + """Test get RPC input triggers.""" + monkeypatch.setattr(mock_rpc_device, "config", {"input:0": {"type": "button"}}) + assert set(get_rpc_input_triggers(mock_rpc_device)) == { + ("long_push", "button1"), + ("single_push", "button1"), + ("btn_down", "button1"), + ("double_push", "button1"), + ("btn_up", "button1"), + } + + monkeypatch.setattr(mock_rpc_device, "config", {"input:0": {"type": "switch"}}) + assert not get_rpc_input_triggers(mock_rpc_device) diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 165f71cde04..4b8686d7a7f 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry, load_fixture CODE = "12345" PASSWORD = "password" -SYSTEM_ID = "system_123" +SYSTEM_ID = 12345 @pytest.fixture(name="api") @@ -79,7 +79,8 @@ def data_settings_fixture(): @pytest.fixture(name="data_subscription", scope="package") def data_subscription_fixture(): """Define subscription data.""" - return json.loads(load_fixture("subscription_data.json", "simplisafe")) + data = json.loads(load_fixture("subscription_data.json", "simplisafe")) + return {SYSTEM_ID: data} @pytest.fixture(name="reauth_config") diff --git a/tests/components/simplisafe/fixtures/subscription_data.json b/tests/components/simplisafe/fixtures/subscription_data.json index ad83f7a29a2..f0f6f84b17b 100644 --- a/tests/components/simplisafe/fixtures/subscription_data.json +++ b/tests/components/simplisafe/fixtures/subscription_data.json @@ -1,335 +1,333 @@ { - "system_123": { + "uid": 12345, + "sid": 12345, + "sStatus": 20, + "activated": 1445034752, + "planSku": "SSEDSM2", + "planName": "Interactive Monitoring", + "price": 24.99, + "currency": "USD", + "country": "US", + "expires": 1602887552, + "canceled": 0, + "extraTime": 0, + "creditCard": { + "lastFour": "", + "type": "", + "ppid": "ABCDE12345", + "uid": 12345 + }, + "time": 2628000, + "paymentProfileId": "ABCDE12345", + "features": { + "monitoring": true, + "alerts": true, + "online": true, + "hazard": true, + "video": true, + "cameras": 10, + "dispatch": true, + "proInstall": false, + "discount": 0, + "vipCS": false, + "medical": true, + "careVisit": false, + "storageDays": 30 + }, + "status": { + "hasBaseStation": true, + "isActive": true, + "monitoring": "Active" + }, + "subscriptionFeatures": { + "monitoredSensorsTypes": [ + "Entry", + "Motion", + "GlassBreak", + "Smoke", + "CO", + "Freeze", + "Water" + ], + "monitoredPanicConditions": ["Fire", "Medical", "Duress"], + "dispatchTypes": ["Police", "Fire", "Medical", "Guard"], + "remoteControl": [ + "ArmDisarm", + "LockUnlock", + "ViewSettings", + "ConfigureSettings" + ], + "cameraFeatures": { + "liveView": true, + "maxRecordingCameras": 10, + "recordingStorageDays": 30, + "videoVerification": true + }, + "support": { + "level": "Basic", + "annualVisit": false, + "professionalInstall": false + }, + "cellCommunicationBackup": true, + "alertChannels": ["Push", "SMS", "Email"], + "alertTypes": ["Alarm", "Error", "Activity", "Camera"], + "alarmModes": ["Alarm", "SecretAlert", "Disabled"], + "supportedIntegrations": ["GoogleAssistant", "AmazonAlexa", "AugustLock"], + "timeline": {} + }, + "dispatcher": "cops", + "dcid": 0, + "location": { + "sid": 12345, "uid": 12345, - "sid": "system_123", - "sStatus": 20, - "activated": 1445034752, - "planSku": "SSEDSM2", - "planName": "Interactive Monitoring", - "price": 24.99, - "currency": "USD", + "lStatus": 10, + "account": "1234ABCD", + "street1": "1234 Main Street", + "street2": "", + "locationName": "", + "city": "Atlantis", + "county": "SEA", + "state": "UW", + "zip": "12345", "country": "US", - "expires": 1602887552, - "canceled": 0, - "extraTime": 0, - "creditCard": { - "lastFour": "", - "type": "", - "ppid": "ABCDE12345", - "uid": 12345 - }, - "time": 2628000, - "paymentProfileId": "ABCDE12345", - "features": { - "monitoring": true, - "alerts": true, - "online": true, - "hazard": true, - "video": true, - "cameras": 10, - "dispatch": true, - "proInstall": false, - "discount": 0, - "vipCS": false, - "medical": true, - "careVisit": false, - "storageDays": 30 - }, - "status": { - "hasBaseStation": true, - "isActive": true, - "monitoring": "Active" - }, - "subscriptionFeatures": { - "monitoredSensorsTypes": [ - "Entry", - "Motion", - "GlassBreak", - "Smoke", - "CO", - "Freeze", - "Water" - ], - "monitoredPanicConditions": ["Fire", "Medical", "Duress"], - "dispatchTypes": ["Police", "Fire", "Medical", "Guard"], - "remoteControl": [ - "ArmDisarm", - "LockUnlock", - "ViewSettings", - "ConfigureSettings" - ], - "cameraFeatures": { - "liveView": true, - "maxRecordingCameras": 10, - "recordingStorageDays": 30, - "videoVerification": true + "crossStreet": "River 1 and River 2", + "notes": "", + "residenceType": 2, + "numAdults": 2, + "numChildren": 0, + "locationOffset": -360, + "safeWord": "TRITON", + "signature": "Atlantis Citizen 1", + "timeZone": 2, + "primaryContacts": [ + { + "name": "John Doe", + "phone": "1234567890" + } + ], + "secondaryContacts": [ + { + "name": "Jane Doe", + "phone": "9876543210" + } + ], + "copsOptIn": false, + "certificateUri": "https://simplisafe.com/account2/12345/alarm-certificate/12345", + "nestStructureId": "", + "system": { + "serial": "1234ABCD", + "alarmState": "OFF", + "alarmStateTimestamp": 0, + "isAlarming": false, + "version": 3, + "capabilities": { + "setWifiOverCell": true, + "setDoorbellChimeVolume": true, + "outdoorBattCamera": true }, - "support": { - "level": "Basic", - "annualVisit": false, - "professionalInstall": false - }, - "cellCommunicationBackup": true, - "alertChannels": ["Push", "SMS", "Email"], - "alertTypes": ["Alarm", "Error", "Activity", "Camera"], - "alarmModes": ["Alarm", "SecretAlert", "Disabled"], - "supportedIntegrations": ["GoogleAssistant", "AmazonAlexa", "AugustLock"], - "timeline": {} - }, - "dispatcher": "cops", - "dcid": 0, - "location": { - "sid": 12345, - "uid": 12345, - "lStatus": 10, - "account": "1234ABCD", - "street1": "1234 Main Street", - "street2": "", - "locationName": "", - "city": "Atlantis", - "county": "SEA", - "state": "UW", - "zip": "12345", - "country": "US", - "crossStreet": "River 1 and River 2", - "notes": "", - "residenceType": 2, - "numAdults": 2, - "numChildren": 0, - "locationOffset": -360, - "safeWord": "TRITON", - "signature": "Atlantis Citizen 1", - "timeZone": 2, - "primaryContacts": [ + "temperature": 67, + "exitDelayRemaining": 60, + "cameras": [ { - "name": "John Doe", - "phone": "1234567890" - } - ], - "secondaryContacts": [ - { - "name": "Jane Doe", - "phone": "9876543210" - } - ], - "copsOptIn": false, - "certificateUri": "https://simplisafe.com/account2/12345/alarm-certificate/12345", - "nestStructureId": "", - "system": { - "serial": "1234ABCD", - "alarmState": "OFF", - "alarmStateTimestamp": 0, - "isAlarming": false, - "version": 3, - "capabilities": { - "setWifiOverCell": true, - "setDoorbellChimeVolume": true, - "outdoorBattCamera": true - }, - "temperature": 67, - "exitDelayRemaining": 60, - "cameras": [ - { - "staleSettingsTypes": [], - "upgradeWhitelisted": false, - "model": "SS001", - "uuid": "1234567890", - "uid": 12345, - "sid": 12345, - "cameraSettings": { - "cameraName": "Camera", - "pictureQuality": "720p", - "nightVision": "auto", - "statusLight": "off", - "micSensitivity": 100, - "micEnable": true, - "speakerVolume": 75, - "motionSensitivity": 0, - "shutterHome": "closedAlarmOnly", - "shutterAway": "open", - "shutterOff": "closedAlarmOnly", - "wifiSsid": "", - "canStream": false, - "canRecord": false, - "pirEnable": true, - "vaEnable": true, - "notificationsEnable": false, - "enableDoorbellNotification": true, - "doorbellChimeVolume": "off", - "privacyEnable": false, - "hdr": false, - "vaZoningEnable": false, - "vaZoningRows": 0, - "vaZoningCols": 0, - "vaZoningMask": [], - "maxDigitalZoom": 10, - "supportedResolutions": ["480p", "720p"], - "admin": { - "IRLED": 0, - "pirSens": 0, - "statusLEDState": 1, - "lux": "lowLux", - "motionDetectionEnabled": false, - "motionThresholdZero": 0, - "motionThresholdOne": 10000, - "levelChangeDelayZero": 30, - "levelChangeDelayOne": 10, - "audioDetectionEnabled": false, - "audioChannelNum": 2, - "audioSampleRate": 16000, - "audioChunkBytes": 2048, - "audioSampleFormat": 3, - "audioSensitivity": 50, - "audioThreshold": 50, - "audioDirection": 0, - "bitRate": 284, - "longPress": 2000, - "kframe": 1, - "gopLength": 40, - "idr": 1, - "fps": 20, - "firmwareVersion": "2.6.1.107", - "netConfigVersion": "", - "camAgentVersion": "", - "lastLogin": 1600639997, - "lastLogout": 1600639944, - "pirSampleRateMs": 800, - "pirHysteresisHigh": 2, - "pirHysteresisLow": 10, - "pirFilterCoefficient": 1, - "logEnabled": true, - "logLevel": 3, - "logQDepth": 20, - "firmwareGroup": "public", - "irOpenThreshold": 445, - "irCloseThreshold": 840, - "irOpenDelay": 3, - "irCloseDelay": 3, - "irThreshold1x": 388, - "irThreshold2x": 335, - "irThreshold3x": 260, - "rssi": [[1600935204, -43]], - "battery": [], - "dbm": 0, - "vmUse": 161592, - "resSet": 10540, - "uptime": 810043.74, - "wifiDisconnects": 1, - "wifiDriverReloads": 1, - "statsPeriod": 3600000, - "sarlaccDebugLogTypes": 0, - "odProcessingFps": 8, - "odObjectMinWidthPercent": 6, - "odObjectMinHeightPercent": 24, - "odEnableObjectDetection": true, - "odClassificationMask": 2, - "odClassificationConfidenceThreshold": 0.95, - "odEnableOverlay": false, - "odAnalyticsLib": 2, - "odSensitivity": 85, - "odEventObjectMask": 2, - "odLuxThreshold": 445, - "odLuxHysteresisHigh": 4, - "odLuxHysteresisLow": 4, - "odLuxSamplingFrequency": 30, - "odFGExtractorMode": 2, - "odVideoScaleFactor": 1, - "odSceneType": 1, - "odCameraView": 3, - "odCameraFOV": 2, - "odBackgroundLearnStationary": true, - "odBackgroundLearnStationarySpeed": 15, - "odClassifierQualityProfile": 1, - "odEnableVideoAnalyticsWhileStreaming": false, - "wlanMac": "XX:XX:XX:XX:XX:XX", - "region": "us-east-1", - "enableWifiAnalyticsLib": false, - "ivLicense": "" - }, - "pirLevel": "medium", - "odLevel": "medium" - }, - "__v": 0, - "cameraStatus": { + "staleSettingsTypes": [], + "upgradeWhitelisted": false, + "model": "SS001", + "uuid": "1234567890", + "uid": 12345, + "sid": 12345, + "cameraSettings": { + "cameraName": "Camera", + "pictureQuality": "720p", + "nightVision": "auto", + "statusLight": "off", + "micSensitivity": 100, + "micEnable": true, + "speakerVolume": 75, + "motionSensitivity": 0, + "shutterHome": "closedAlarmOnly", + "shutterAway": "open", + "shutterOff": "closedAlarmOnly", + "wifiSsid": "", + "canStream": false, + "canRecord": false, + "pirEnable": true, + "vaEnable": true, + "notificationsEnable": false, + "enableDoorbellNotification": true, + "doorbellChimeVolume": "off", + "privacyEnable": false, + "hdr": false, + "vaZoningEnable": false, + "vaZoningRows": 0, + "vaZoningCols": 0, + "vaZoningMask": [], + "maxDigitalZoom": 10, + "supportedResolutions": ["480p", "720p"], + "admin": { + "IRLED": 0, + "pirSens": 0, + "statusLEDState": 1, + "lux": "lowLux", + "motionDetectionEnabled": false, + "motionThresholdZero": 0, + "motionThresholdOne": 10000, + "levelChangeDelayZero": 30, + "levelChangeDelayOne": 10, + "audioDetectionEnabled": false, + "audioChannelNum": 2, + "audioSampleRate": 16000, + "audioChunkBytes": 2048, + "audioSampleFormat": 3, + "audioSensitivity": 50, + "audioThreshold": 50, + "audioDirection": 0, + "bitRate": 284, + "longPress": 2000, + "kframe": 1, + "gopLength": 40, + "idr": 1, + "fps": 20, "firmwareVersion": "2.6.1.107", "netConfigVersion": "", "camAgentVersion": "", "lastLogin": 1600639997, "lastLogout": 1600639944, + "pirSampleRateMs": 800, + "pirHysteresisHigh": 2, + "pirHysteresisLow": 10, + "pirFilterCoefficient": 1, + "logEnabled": true, + "logLevel": 3, + "logQDepth": 20, + "firmwareGroup": "public", + "irOpenThreshold": 445, + "irCloseThreshold": 840, + "irOpenDelay": 3, + "irCloseDelay": 3, + "irThreshold1x": 388, + "irThreshold2x": 335, + "irThreshold3x": 260, + "rssi": [[1600935204, -43]], + "battery": [], + "dbm": 0, + "vmUse": 161592, + "resSet": 10540, + "uptime": 810043.74, + "wifiDisconnects": 1, + "wifiDriverReloads": 1, + "statsPeriod": 3600000, + "sarlaccDebugLogTypes": 0, + "odProcessingFps": 8, + "odObjectMinWidthPercent": 6, + "odObjectMinHeightPercent": 24, + "odEnableObjectDetection": true, + "odClassificationMask": 2, + "odClassificationConfidenceThreshold": 0.95, + "odEnableOverlay": false, + "odAnalyticsLib": 2, + "odSensitivity": 85, + "odEventObjectMask": 2, + "odLuxThreshold": 445, + "odLuxHysteresisHigh": 4, + "odLuxHysteresisLow": 4, + "odLuxSamplingFrequency": 30, + "odFGExtractorMode": 2, + "odVideoScaleFactor": 1, + "odSceneType": 1, + "odCameraView": 3, + "odCameraFOV": 2, + "odBackgroundLearnStationary": true, + "odBackgroundLearnStationarySpeed": 15, + "odClassifierQualityProfile": 1, + "odEnableVideoAnalyticsWhileStreaming": false, "wlanMac": "XX:XX:XX:XX:XX:XX", - "fwDownloadVersion": "", - "fwDownloadPercentage": 0, - "recovered": false, - "recoveredFromVersion": "", - "_id": "1234567890", - "initErrors": [], - "speedTestTokenCreated": 1600235629 + "region": "us-east-1", + "enableWifiAnalyticsLib": false, + "ivLicense": "" }, - "supportedFeatures": { - "providers": { - "webrtc": "none", - "recording": "simplisafe", - "live": "simplisafe" - }, - "audioEncodings": ["speex"], - "resolutions": ["480p", "720p"], - "_id": "1234567890", - "pir": true, - "videoAnalytics": false, - "privacyShutter": true, - "microphone": true, - "fullDuplexAudio": false, - "wired": true, - "networkSpeedTest": false, - "videoEncoding": "h264" + "pirLevel": "medium", + "odLevel": "medium" + }, + "__v": 0, + "cameraStatus": { + "firmwareVersion": "2.6.1.107", + "netConfigVersion": "", + "camAgentVersion": "", + "lastLogin": 1600639997, + "lastLogout": 1600639944, + "wlanMac": "XX:XX:XX:XX:XX:XX", + "fwDownloadVersion": "", + "fwDownloadPercentage": 0, + "recovered": false, + "recoveredFromVersion": "", + "_id": "1234567890", + "initErrors": [], + "speedTestTokenCreated": 1600235629 + }, + "supportedFeatures": { + "providers": { + "webrtc": "none", + "recording": "simplisafe", + "live": "simplisafe" }, - "subscription": { - "enabled": true, - "freeTrialActive": false, - "freeTrialUsed": true, - "freeTrialEnds": 0, - "freeTrialExpires": 0, - "planSku": "SSVM1", - "price": 0, - "expires": 0, - "storageDays": 30, - "trialUsed": true, - "trialActive": false, - "trialExpires": 0 - }, - "status": "online" - } - ], - "connType": "wifi", - "stateUpdated": 1601502948, - "messages": [ - { - "_id": "xxxxxxxxxxxxxxxxxxxxxxxx", - "id": "xxxxxxxxxxxxxxxxxxxxxxxx", - "textTemplate": "Power Outage - Backup battery in use.", - "data": { - "time": "2020-02-16T03:20:28+00:00" - }, - "text": "Power Outage - Backup battery in use.", - "code": "2000", - "filters": [], - "link": "http://link.to.info", - "linkLabel": "More Info", - "expiration": 0, - "category": "error", - "timestamp": 1581823228 - } - ], - "powerOutage": false, - "lastPowerOutage": 1581991064, - "lastSuccessfulWifiTS": 1601424776, - "isOffline": false - } - }, - "pinUnlocked": true, - "billDate": 1602887552, - "billInterval": 2628000, - "pinUnlockedBy": "pin", - "autoActivation": null - } + "audioEncodings": ["speex"], + "resolutions": ["480p", "720p"], + "_id": "1234567890", + "pir": true, + "videoAnalytics": false, + "privacyShutter": true, + "microphone": true, + "fullDuplexAudio": false, + "wired": true, + "networkSpeedTest": false, + "videoEncoding": "h264" + }, + "subscription": { + "enabled": true, + "freeTrialActive": false, + "freeTrialUsed": true, + "freeTrialEnds": 0, + "freeTrialExpires": 0, + "planSku": "SSVM1", + "price": 0, + "expires": 0, + "storageDays": 30, + "trialUsed": true, + "trialActive": false, + "trialExpires": 0 + }, + "status": "online" + } + ], + "connType": "wifi", + "stateUpdated": 1601502948, + "messages": [ + { + "_id": "xxxxxxxxxxxxxxxxxxxxxxxx", + "id": "xxxxxxxxxxxxxxxxxxxxxxxx", + "textTemplate": "Power Outage - Backup battery in use.", + "data": { + "time": "2020-02-16T03:20:28+00:00" + }, + "text": "Power Outage - Backup battery in use.", + "code": "2000", + "filters": [], + "link": "http://link.to.info", + "linkLabel": "More Info", + "expiration": 0, + "category": "error", + "timestamp": 1581823228 + } + ], + "powerOutage": false, + "lastPowerOutage": 1581991064, + "lastSuccessfulWifiTS": 1601424776, + "isOffline": false + } + }, + "pinUnlocked": true, + "billDate": 1602887552, + "billInterval": 2628000, + "pinUnlockedBy": "pin", + "autoActivation": null } diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index f7a88fe0d06..30e52021f6f 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -21,7 +21,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa "disabled_by": None, }, "subscription_data": { - "system_123": { + "12345": { "uid": REDACTED, "sid": REDACTED, "sStatus": 20, diff --git a/tests/components/simplisafe/test_init.py b/tests/components/simplisafe/test_init.py new file mode 100644 index 00000000000..6b81bbe7943 --- /dev/null +++ b/tests/components/simplisafe/test_init.py @@ -0,0 +1,40 @@ +"""Define tests for SimpliSafe setup.""" +from unittest.mock import patch + +from homeassistant.components.simplisafe import DOMAIN +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + + +async def test_base_station_migration(hass, api, config, config_entry): + """Test that errors are shown when duplicates are added.""" + old_identifers = (DOMAIN, 12345) + new_identifiers = (DOMAIN, "12345") + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={old_identifers}, + manufacturer="SimpliSafe", + name="old", + ) + + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + return_value=api, + ), patch( + "homeassistant.components.simplisafe.API.async_from_auth", + return_value=api, + ), patch( + "homeassistant.components.simplisafe.API.async_from_refresh_token", + return_value=api, + ), patch( + "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" + ), patch( + "homeassistant.components.simplisafe.PLATFORMS", [] + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + assert device_registry.async_get_device(identifiers={old_identifers}) is None + assert device_registry.async_get_device(identifiers={new_identifiers}) is not None diff --git a/tests/components/skybell/__init__.py b/tests/components/skybell/__init__.py index dd162ed5d80..fc049adcc3d 100644 --- a/tests/components/skybell/__init__.py +++ b/tests/components/skybell/__init__.py @@ -1,7 +1,5 @@ """Tests for the SkyBell integration.""" -from unittest.mock import AsyncMock, patch - from homeassistant.const import CONF_EMAIL, CONF_PASSWORD USERNAME = "user" @@ -12,19 +10,3 @@ CONF_CONFIG_FLOW = { CONF_EMAIL: USERNAME, CONF_PASSWORD: PASSWORD, } - - -def _patch_skybell_devices() -> None: - mocked_skybell = AsyncMock() - mocked_skybell.user_id = USER_ID - return patch( - "homeassistant.components.skybell.config_flow.Skybell.async_get_devices", - return_value=[mocked_skybell], - ) - - -def _patch_skybell() -> None: - return patch( - "homeassistant.components.skybell.config_flow.Skybell.async_send_request", - return_value={"id": USER_ID}, - ) diff --git a/tests/components/skybell/conftest.py b/tests/components/skybell/conftest.py new file mode 100644 index 00000000000..46337f612e1 --- /dev/null +++ b/tests/components/skybell/conftest.py @@ -0,0 +1,25 @@ +"""Test setup for the SkyBell integration.""" + +from unittest.mock import AsyncMock, patch + +from aioskybell import Skybell, SkybellDevice +from pytest import fixture + +from . import USER_ID + + +@fixture(autouse=True) +def skybell_mock(): + """Fixture for our skybell tests.""" + mocked_skybell_device = AsyncMock(spec=SkybellDevice) + + mocked_skybell = AsyncMock(spec=Skybell) + mocked_skybell.async_get_devices.return_value = [mocked_skybell_device] + mocked_skybell.async_send_request.return_value = {"id": USER_ID} + mocked_skybell.user_id = USER_ID + + with patch( + "homeassistant.components.skybell.config_flow.Skybell", + return_value=mocked_skybell, + ), patch("homeassistant.components.skybell.Skybell", return_value=mocked_skybell): + yield mocked_skybell diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 9b7afd12c93..318819246a3 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from aioskybell import exceptions +from pytest import fixture from homeassistant import config_entries from homeassistant.components.skybell.const import DOMAIN @@ -10,43 +11,39 @@ from homeassistant.const import CONF_PASSWORD, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import ( - CONF_CONFIG_FLOW, - PASSWORD, - USER_ID, - _patch_skybell, - _patch_skybell_devices, -) +from . import CONF_CONFIG_FLOW, PASSWORD, USER_ID from tests.common import MockConfigEntry -def _patch_setup_entry() -> None: - return patch( +@fixture(autouse=True) +def setup_entry() -> None: + """Make sure component doesn't initialize.""" + with patch( "homeassistant.components.skybell.async_setup_entry", return_value=True, - ) + ): + yield async def test_flow_user(hass: HomeAssistant) -> None: """Test that the user step works.""" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=CONF_CONFIG_FLOW, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_CONFIG_FLOW, + ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "user" - assert result["data"] == CONF_CONFIG_FLOW - assert result["result"].unique_id == USER_ID + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "user" + assert result["data"] == CONF_CONFIG_FLOW + assert result["result"].unique_id == USER_ID async def test_flow_user_already_configured(hass: HomeAssistant) -> None: @@ -57,50 +54,48 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with _patch_skybell(), _patch_skybell_devices(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: +async def test_flow_user_cannot_connect(hass: HomeAssistant, skybell_mock) -> None: """Test user initialized flow with unreachable server.""" - with _patch_skybell() as skybell_mock: - skybell_mock.side_effect = exceptions.SkybellException(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + skybell_mock.async_initialize.side_effect = exceptions.SkybellException(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} -async def test_invalid_credentials(hass: HomeAssistant) -> None: +async def test_invalid_credentials(hass: HomeAssistant, skybell_mock) -> None: """Test that invalid credentials throws an error.""" - with patch("homeassistant.components.skybell.Skybell.async_login") as skybell_mock: - skybell_mock.side_effect = exceptions.SkybellAuthenticationException(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) + skybell_mock.async_initialize.side_effect = ( + exceptions.SkybellAuthenticationException(hass) + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_auth"} + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} -async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: +async def test_flow_user_unknown_error(hass: HomeAssistant, skybell_mock) -> None: """Test user initialized flow with unreachable server.""" - with _patch_skybell_devices() as skybell_mock: - skybell_mock.side_effect = Exception - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "unknown"} + skybell_mock.async_initialize.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} async def test_step_reauth(hass: HomeAssistant) -> None: @@ -121,17 +116,15 @@ async def test_step_reauth(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" -async def test_step_reauth_failed(hass: HomeAssistant) -> None: +async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: """Test the reauth flow fails and recovers.""" entry = MockConfigEntry(domain=DOMAIN, unique_id=USER_ID, data=CONF_CONFIG_FLOW) entry.add_to_hass(hass) @@ -149,21 +142,22 @@ async def test_step_reauth_failed(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch("homeassistant.components.skybell.Skybell.async_login") as skybell_mock: - skybell_mock.side_effect = exceptions.SkybellAuthenticationException(hass) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: PASSWORD}, - ) + skybell_mock.async_initialize.side_effect = ( + exceptions.SkybellAuthenticationException(hass) + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): + skybell_mock.async_initialize.side_effect = None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 14e58d54ebe..9582c83b267 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -28,6 +28,23 @@ async def test_snips_config(hass, mqtt_mock): assert result +async def test_snips_no_mqtt(hass, caplog): + """Test Snips Config.""" + result = await async_setup_component( + hass, + "snips", + { + "snips": { + "feedback_sounds": True, + "probability_threshold": 0.5, + "site_ids": ["default", "remote"], + } + }, + ) + assert not result + assert "MQTT integration is not available" in caplog.text + + async def test_snips_bad_config(hass, mqtt_mock): """Test Snips bad config.""" result = await async_setup_component( diff --git a/tests/components/statistics/fixtures/configuration.yaml b/tests/components/statistics/fixtures/configuration.yaml index 4708910b53e..b7a10756281 100644 --- a/tests/components/statistics/fixtures/configuration.yaml +++ b/tests/components/statistics/fixtures/configuration.yaml @@ -3,3 +3,4 @@ sensor: entity_id: sensor.cpu name: cputest state_characteristic: mean + sampling_size: 20 diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 691159fe2fc..bd73216d69e 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -45,8 +45,10 @@ async def test_unique_id(hass: HomeAssistant): { "platform": "statistics", "name": "test", - "entity_id": "sensor.test_monitored", "unique_id": "uniqueid_sensor_test", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 20, }, ] }, @@ -71,6 +73,8 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 20, }, ] }, @@ -162,6 +166,8 @@ async def test_sensor_defaults_binary(hass: HomeAssistant): "platform": "statistics", "name": "test", "entity_id": "binary_sensor.test_monitored", + "state_characteristic": "count", + "sampling_size": 20, }, ] }, @@ -199,12 +205,14 @@ async def test_sensor_source_with_force_update(hass: HomeAssistant): "name": "test_normal", "entity_id": "sensor.test_monitored_normal", "state_characteristic": "mean", + "sampling_size": 20, }, { "platform": "statistics", "name": "test_force", "entity_id": "sensor.test_monitored_force", "state_characteristic": "mean", + "sampling_size": 20, }, ] }, @@ -234,8 +242,8 @@ async def test_sensor_source_with_force_update(hass: HomeAssistant): assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) -async def test_sampling_size_non_default(hass: HomeAssistant): - """Test rotation.""" +async def test_sampling_size_reduced(hass: HomeAssistant): + """Test limited buffer size.""" assert await async_setup_component( hass, "sensor", @@ -287,7 +295,7 @@ async def test_sampling_size_1(hass: HomeAssistant): ) await hass.async_block_till_done() - for value in VALUES_NUMERIC[-3:]: # just the last 3 will do + for value in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored", str(value), @@ -303,7 +311,7 @@ async def test_sampling_size_1(hass: HomeAssistant): async def test_age_limit_expiry(hass: HomeAssistant): - """Test that values are removed after certain age.""" + """Test that values are removed with given max age.""" now = dt_util.utcnow() mock_data = { "return_time": datetime(now.year + 1, 8, 2, 12, 23, tzinfo=dt_util.UTC) @@ -325,6 +333,7 @@ async def test_age_limit_expiry(hass: HomeAssistant): "name": "test", "entity_id": "sensor.test_monitored", "state_characteristic": "mean", + "sampling_size": 20, "max_age": {"minutes": 4}, }, ] @@ -391,7 +400,7 @@ async def test_age_limit_expiry(hass: HomeAssistant): async def test_precision(hass: HomeAssistant): - """Test correct result with precision set.""" + """Test correct results with precision set.""" assert await async_setup_component( hass, "sensor", @@ -402,6 +411,7 @@ async def test_precision(hass: HomeAssistant): "name": "test_precision_0", "entity_id": "sensor.test_monitored", "state_characteristic": "mean", + "sampling_size": 20, "precision": 0, }, { @@ -409,6 +419,7 @@ async def test_precision(hass: HomeAssistant): "name": "test_precision_3", "entity_id": "sensor.test_monitored", "state_characteristic": "mean", + "sampling_size": 20, "precision": 3, }, ] @@ -433,6 +444,60 @@ async def test_precision(hass: HomeAssistant): assert state.state == str(round(mean, 3)) +async def test_percentile(hass: HomeAssistant): + """Test correct results for percentile characteristic.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_percentile_omitted", + "entity_id": "sensor.test_monitored", + "state_characteristic": "percentile", + "sampling_size": 20, + }, + { + "platform": "statistics", + "name": "test_percentile_default", + "entity_id": "sensor.test_monitored", + "state_characteristic": "percentile", + "sampling_size": 20, + "percentile": 50, + }, + { + "platform": "statistics", + "name": "test_percentile_min", + "entity_id": "sensor.test_monitored", + "state_characteristic": "percentile", + "sampling_size": 20, + "percentile": 1, + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + str(value), + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_percentile_omitted") + assert state is not None + assert state.state == str(9.2) + state = hass.states.get("sensor.test_percentile_default") + assert state is not None + assert state.state == str(9.2) + state = hass.states.get("sensor.test_percentile_min") + assert state is not None + assert state.state == str(2.72) + + async def test_device_class(hass: HomeAssistant): """Test device class, which depends on the source entity.""" assert await async_setup_component( @@ -446,6 +511,7 @@ async def test_device_class(hass: HomeAssistant): "name": "test_source_class", "entity_id": "sensor.test_monitored", "state_characteristic": "mean", + "sampling_size": 20, }, { # Device class is set to None for characteristics with special meaning @@ -453,6 +519,7 @@ async def test_device_class(hass: HomeAssistant): "name": "test_none", "entity_id": "sensor.test_monitored", "state_characteristic": "count", + "sampling_size": 20, }, { # Device class is set to timestamp for datetime characteristics @@ -460,6 +527,7 @@ async def test_device_class(hass: HomeAssistant): "name": "test_timestamp", "entity_id": "sensor.test_monitored", "state_characteristic": "datetime_oldest", + "sampling_size": 20, }, ] }, @@ -500,12 +568,14 @@ async def test_state_class(hass: HomeAssistant): "name": "test_normal", "entity_id": "sensor.test_monitored", "state_characteristic": "count", + "sampling_size": 20, }, { "platform": "statistics", "name": "test_nan", "entity_id": "sensor.test_monitored", "state_characteristic": "datetime_oldest", + "sampling_size": 20, }, ] }, @@ -540,29 +610,35 @@ async def test_unitless_source_sensor(hass: HomeAssistant): "name": "test_unitless_1", "entity_id": "sensor.test_monitored_unitless", "state_characteristic": "count", + "sampling_size": 20, }, { "platform": "statistics", "name": "test_unitless_2", "entity_id": "sensor.test_monitored_unitless", "state_characteristic": "mean", + "sampling_size": 20, }, { "platform": "statistics", "name": "test_unitless_3", "entity_id": "sensor.test_monitored_unitless", "state_characteristic": "change_second", + "sampling_size": 20, }, { "platform": "statistics", "name": "test_unitless_4", "entity_id": "binary_sensor.test_monitored_unitless", + "state_characteristic": "count", + "sampling_size": 20, }, { "platform": "statistics", "name": "test_unitless_5", "entity_id": "binary_sensor.test_monitored_unitless", "state_characteristic": "mean", + "sampling_size": 20, }, ] }, @@ -753,13 +829,11 @@ async def test_state_characteristics(hass: HomeAssistant): }, { "source_sensor_domain": "sensor", - "name": "quantiles", + "name": "percentile", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, - "value_9": [ - round(quantile, 2) for quantile in statistics.quantiles(VALUES_NUMERIC) - ], - "unit": None, + "value_9": 9.2, + "unit": "°C", }, { "source_sensor_domain": "sensor", @@ -1035,12 +1109,14 @@ async def test_invalid_state_characteristic(hass: HomeAssistant): "name": "test_numeric", "entity_id": "sensor.test_monitored", "state_characteristic": "invalid", + "sampling_size": 20, }, { "platform": "statistics", "name": "test_binary", "entity_id": "binary_sensor.test_monitored", "state_characteristic": "variance", + "sampling_size": 20, }, ] }, @@ -1140,8 +1216,8 @@ async def test_initialize_from_database_with_maxage(recorder_mock, hass: HomeAss "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", - "sampling_size": 100, "state_characteristic": "datetime_newest", + "sampling_size": 100, "max_age": {"hours": 3}, }, ] diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index de5b2c234eb..ff98d90ea8d 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -8,7 +8,8 @@ import io import av import numpy as np -from homeassistant.components.stream.core import Segment +from homeassistant.components.camera import DynamicStreamSettings +from homeassistant.components.stream.core import Orientation, Segment from homeassistant.components.stream.fmp4utils import ( TRANSFORM_MATRIX_TOP, XYW_ROW, @@ -16,8 +17,8 @@ from homeassistant.components.stream.fmp4utils import ( ) FAKE_TIME = datetime.utcnow() -# Segment with defaults filled in for use in tests +# Segment with defaults filled in for use in tests DefaultSegment = partial( Segment, init=None, @@ -157,7 +158,7 @@ def remux_with_audio(source, container_format, audio_codec): return output -def assert_mp4_has_transform_matrix(mp4: bytes, orientation: int): +def assert_mp4_has_transform_matrix(mp4: bytes, orientation: Orientation): """Assert that the mp4 (or init) has the proper transformation matrix.""" # Find moov moov_location = next(find_box(mp4, b"moov")) @@ -170,3 +171,8 @@ def assert_mp4_has_transform_matrix(mp4: bytes, orientation: int): mp4[tkhd_location + tkhd_length - 44 : tkhd_location + tkhd_length - 8] == TRANSFORM_MATRIX_TOP[orientation] + XYW_ROW ) + + +def dynamic_stream_settings(): + """Create new dynamic stream settings.""" + return DynamicStreamSettings() diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index e9da793369f..cd4f5796938 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -16,7 +16,7 @@ from homeassistant.components.stream.const import ( MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS, ) -from homeassistant.components.stream.core import Part +from homeassistant.components.stream.core import Orientation, Part from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -24,6 +24,7 @@ from .common import ( FAKE_TIME, DefaultSegment as Segment, assert_mp4_has_transform_matrix, + dynamic_stream_settings, ) from tests.common import async_fire_time_changed @@ -145,7 +146,7 @@ async def test_hls_stream( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -185,7 +186,7 @@ async def test_hls_stream( assert stream.get_diagnostics() == { "container_format": "mov,mp4,m4a,3gp,3g2,mj2", "keepalive": False, - "orientation": 1, + "orientation": Orientation.NO_TRANSFORM, "start_worker": 1, "video_codec": "h264", "worker_error": 1, @@ -199,7 +200,7 @@ async def test_stream_timeout( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) available_states = [] @@ -252,7 +253,7 @@ async def test_stream_timeout_after_stop( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -272,7 +273,7 @@ async def test_stream_retries(hass, setup_component, should_retry): """Test hls stream is retried on failure.""" # Setup demo HLS track source = "test_stream_keepalive_source" - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, dynamic_stream_settings()) track = stream.add_provider(HLS_PROVIDER) track.num_segments = 2 @@ -320,7 +321,7 @@ async def test_stream_retries(hass, setup_component, should_retry): async def test_hls_playlist_view_no_output(hass, setup_component, hls_stream): """Test rendering the hls playlist with no output segments.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream.add_provider(HLS_PROVIDER) hls_client = await hls_stream(stream) @@ -332,7 +333,7 @@ async def test_hls_playlist_view_no_output(hass, setup_component, hls_stream): async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worker_sync): """Test rendering the hls playlist with 1 and 2 output segments.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) for i in range(2): @@ -363,7 +364,7 @@ async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worke async def test_hls_max_segments(hass, setup_component, hls_stream, stream_worker_sync): """Test rendering the hls playlist with more segments than the segment deque can hold.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -415,7 +416,7 @@ async def test_hls_playlist_view_discontinuity( ): """Test a discontinuity across segments in the stream with 3 segments.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -452,7 +453,7 @@ async def test_hls_max_segments_discontinuity( hass, setup_component, hls_stream, stream_worker_sync ): """Test a discontinuity with more segments than the segment deque can hold.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -495,7 +496,7 @@ async def test_remove_incomplete_segment_on_exit( hass, setup_component, stream_worker_sync ): """Test that the incomplete segment gets removed when the worker thread quits.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() await stream.start() hls = stream.add_provider(HLS_PROVIDER) @@ -536,7 +537,7 @@ async def test_hls_stream_rotate( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -549,14 +550,14 @@ async def test_hls_stream_rotate( assert master_playlist_response.status == HTTPStatus.OK # Fetch rotated init - stream.orientation = 6 + stream.dynamic_stream_settings.orientation = Orientation.ROTATE_LEFT init_response = await hls_client.get("/init.mp4") assert init_response.status == HTTPStatus.OK init = await init_response.read() stream_worker_sync.resume() - assert_mp4_has_transform_matrix(init, stream.orientation) + assert_mp4_has_transform_matrix(init, stream.dynamic_stream_settings.orientation) # Stop stream, if it hasn't quit already await stream.stop() diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index baad3043547..448c3593d68 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -22,7 +22,12 @@ from homeassistant.components.stream.const import ( from homeassistant.components.stream.core import Part from homeassistant.setup import async_setup_component -from .common import FAKE_TIME, DefaultSegment as Segment, generate_h264_video +from .common import ( + FAKE_TIME, + DefaultSegment as Segment, + dynamic_stream_settings, + generate_h264_video, +) from .test_hls import STREAM_SOURCE, HlsClient, make_playlist SEGMENT_DURATION = 6 @@ -135,7 +140,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): num_playlist_segments = 3 # Setup demo HLS track source = generate_h264_video(duration=num_playlist_segments * SEGMENT_DURATION + 2) - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -259,7 +264,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -328,7 +333,7 @@ async def test_ll_hls_msn(hass, hls_stream, stream_worker_sync, hls_sync): }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -393,7 +398,7 @@ async def test_ll_hls_playlist_bad_msn_part(hass, hls_stream, stream_worker_sync }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -462,7 +467,7 @@ async def test_ll_hls_playlist_rollover_part( }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -541,7 +546,7 @@ async def test_ll_hls_playlist_msn_part(hass, hls_stream, stream_worker_sync, hl }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -607,7 +612,7 @@ async def test_get_part_segments(hass, hls_stream, stream_worker_sync, hls_sync) }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index c07675c7712..1e941dd5d81 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -14,7 +14,7 @@ from homeassistant.components.stream.const import ( OUTPUT_IDLE_TIMEOUT, RECORDER_PROVIDER, ) -from homeassistant.components.stream.core import Part +from homeassistant.components.stream.core import Orientation, Part from homeassistant.components.stream.fmp4utils import find_box from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -23,6 +23,7 @@ import homeassistant.util.dt as dt_util from .common import ( DefaultSegment as Segment, assert_mp4_has_transform_matrix, + dynamic_stream_settings, generate_h264_video, remux_with_audio, ) @@ -56,7 +57,7 @@ async def test_record_stream(hass, filename, h264_video): worker_finished.set() with patch("homeassistant.components.stream.Stream", wraps=MockStream): - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -79,7 +80,7 @@ async def test_record_stream(hass, filename, h264_video): async def test_record_lookback(hass, filename, h264_video): """Exercise record with lookback.""" - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Start an HLS feed to enable lookback stream.add_provider(HLS_PROVIDER) @@ -96,7 +97,7 @@ async def test_record_lookback(hass, filename, h264_video): async def test_record_path_not_allowed(hass, h264_video): """Test where the output path is not allowed by home assistant configuration.""" - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) with patch.object( hass.config, "is_allowed_path", return_value=False ), pytest.raises(HomeAssistantError): @@ -146,7 +147,7 @@ async def test_recorder_discontinuity(hass, filename, h264_video): with patch.object(hass.config, "is_allowed_path", return_value=True), patch( "homeassistant.components.stream.Stream", wraps=MockStream ), patch("homeassistant.components.stream.recorder.RecorderOutput.recv"): - stream = create_stream(hass, "blank", {}) + stream = create_stream(hass, "blank", {}, dynamic_stream_settings()) make_recording = hass.async_create_task(stream.async_record(filename)) await provider_ready.wait() @@ -166,7 +167,7 @@ async def test_recorder_discontinuity(hass, filename, h264_video): async def test_recorder_no_segments(hass, filename): """Test recorder behavior with a stream failure which causes no segments.""" - stream = create_stream(hass, BytesIO(), {}) + stream = create_stream(hass, BytesIO(), {}, dynamic_stream_settings()) # Run with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -219,7 +220,7 @@ async def test_record_stream_audio( worker_finished.set() with patch("homeassistant.components.stream.Stream", wraps=MockStream): - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, dynamic_stream_settings()) with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -252,7 +253,9 @@ async def test_record_stream_audio( async def test_recorder_log(hass, filename, caplog): """Test starting a stream to record logs the url without username and password.""" - stream = create_stream(hass, "https://abcd:efgh@foo.bar", {}) + stream = create_stream( + hass, "https://abcd:efgh@foo.bar", {}, dynamic_stream_settings() + ) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record(filename) assert "https://abcd:efgh@foo.bar" not in caplog.text @@ -273,8 +276,8 @@ async def test_record_stream_rotate(hass, filename, h264_video): worker_finished.set() with patch("homeassistant.components.stream.Stream", wraps=MockStream): - stream = create_stream(hass, h264_video, {}) - stream.orientation = 8 + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) + stream.dynamic_stream_settings.orientation = Orientation.ROTATE_RIGHT with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -293,4 +296,6 @@ async def test_record_stream_rotate(hass, filename, h264_video): # Assert assert os.path.exists(filename) with open(filename, "rb") as rotated_mp4: - assert_mp4_has_transform_matrix(rotated_mp4.read(), stream.orientation) + assert_mp4_has_transform_matrix( + rotated_mp4.read(), stream.dynamic_stream_settings.orientation + ) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index e77b062fa9c..a5a1f00d90a 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -39,7 +39,7 @@ from homeassistant.components.stream.const import ( SEGMENT_DURATION_ADJUSTER, TARGET_SEGMENT_DURATION_NON_LL_HLS, ) -from homeassistant.components.stream.core import StreamSettings +from homeassistant.components.stream.core import Orientation, StreamSettings from homeassistant.components.stream.worker import ( StreamEndedError, StreamState, @@ -48,7 +48,7 @@ from homeassistant.components.stream.worker import ( ) from homeassistant.setup import async_setup_component -from .common import generate_h264_video, generate_h265_video +from .common import dynamic_stream_settings, generate_h264_video, generate_h265_video from .test_ll_hls import TEST_PART_DURATION from tests.components.camera.common import EMPTY_8_6_JPEG, mock_turbo_jpeg @@ -90,7 +90,6 @@ def mock_stream_settings(hass): part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, hls_advance_part_limit=3, hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - orientation=1, ) } @@ -287,7 +286,7 @@ def run_worker(hass, stream, stream_source, stream_settings=None): {}, stream_settings or hass.data[DOMAIN][ATTR_SETTINGS], stream_state, - KeyFrameConverter(hass, 1), + KeyFrameConverter(hass, stream_settings, dynamic_stream_settings()), threading.Event(), ) @@ -295,7 +294,11 @@ def run_worker(hass, stream, stream_source, stream_settings=None): async def async_decode_stream(hass, packets, py_av=None, stream_settings=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream( - hass, STREAM_SOURCE, {}, stream_settings or hass.data[DOMAIN][ATTR_SETTINGS] + hass, + STREAM_SOURCE, + {}, + stream_settings or hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), ) stream.add_provider(HLS_PROVIDER) @@ -322,7 +325,13 @@ async def async_decode_stream(hass, packets, py_av=None, stream_settings=None): async def test_stream_open_fails(hass): """Test failure on stream open.""" - stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) + stream = Stream( + hass, + STREAM_SOURCE, + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), + ) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open, pytest.raises(StreamWorkerError): av_open.side_effect = av.error.InvalidDataError(-2, "error") @@ -636,7 +645,13 @@ async def test_stream_stopped_while_decoding(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) + stream = Stream( + hass, + STREAM_SOURCE, + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), + ) stream.add_provider(HLS_PROVIDER) py_av = MockPyAv() @@ -666,7 +681,13 @@ async def test_update_stream_source(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) + stream = Stream( + hass, + STREAM_SOURCE, + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), + ) stream.add_provider(HLS_PROVIDER) # Note that retries are disabled by default in tests, however the stream is "restarted" when # the stream source is updated. @@ -706,22 +727,43 @@ async def test_update_stream_source(hass): await stream.stop() -async def test_worker_log(hass, caplog): +test_worker_log_cases = ( + ("https://abcd:efgh@foo.bar", "https://****:****@foo.bar"), + ( + "https://foo.bar/baz?user=abcd&password=efgh", + "https://foo.bar/baz?user=****&password=****", + ), + ( + "https://foo.bar/baz?param1=abcd¶m2=efgh", + "https://foo.bar/baz?param1=abcd¶m2=efgh", + ), + ( + "https://foo.bar/baz?param1=abcd&password=efgh", + "https://foo.bar/baz?param1=abcd&password=****", + ), +) + + +@pytest.mark.parametrize("stream_url, redacted_url", test_worker_log_cases) +async def test_worker_log(hass, caplog, stream_url, redacted_url): """Test that the worker logs the url without username and password.""" stream = Stream( - hass, "https://abcd:efgh@foo.bar", {}, hass.data[DOMAIN][ATTR_SETTINGS] + hass, + stream_url, + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), ) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open, pytest.raises(StreamWorkerError) as err: av_open.side_effect = av.error.InvalidDataError(-2, "error") - run_worker(hass, stream, "https://abcd:efgh@foo.bar") + run_worker(hass, stream, stream_url) await hass.async_block_till_done() assert ( - str(err.value) - == "Error opening stream (ERRORTYPE_-2, error) https://****:****@foo.bar" + str(err.value) == f"Error opening stream (ERRORTYPE_-2, error) {redacted_url}" ) - assert "https://abcd:efgh@foo.bar" not in caplog.text + assert stream_url not in caplog.text @pytest.fixture @@ -764,7 +806,9 @@ async def test_durations(hass, worker_finished_stream): worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): - stream = create_stream(hass, source, {}, stream_label="camera") + stream = create_stream( + hass, source, {}, dynamic_stream_settings(), stream_label="camera" + ) recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) await stream.start() @@ -839,7 +883,9 @@ async def test_has_keyframe(hass, h264_video, worker_finished_stream): worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): - stream = create_stream(hass, h264_video, {}, stream_label="camera") + stream = create_stream( + hass, h264_video, {}, dynamic_stream_settings(), stream_label="camera" + ) recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) await stream.start() @@ -880,7 +926,9 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream): worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): - stream = create_stream(hass, source, {}, stream_label="camera") + stream = create_stream( + hass, source, {}, dynamic_stream_settings(), stream_label="camera" + ) recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) await stream.start() @@ -900,7 +948,7 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream): assert stream.get_diagnostics() == { "container_format": "mov,mp4,m4a,3gp,3g2,mj2", "keepalive": False, - "orientation": 1, + "orientation": Orientation.NO_TRANSFORM, "start_worker": 1, "video_codec": "hevc", "worker_error": 1, @@ -916,7 +964,7 @@ async def test_get_image(hass, h264_video, filename): "homeassistant.components.camera.img_util.TurboJPEGSingleton" ) as mock_turbo_jpeg_singleton: mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -937,7 +985,6 @@ async def test_worker_disable_ll_hls(hass): part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, hls_advance_part_limit=3, hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - orientation=1, ) py_av = MockPyAv() py_av.container.format.name = "hls" @@ -959,9 +1006,9 @@ async def test_get_image_rotated(hass, h264_video, filename): "homeassistant.components.camera.img_util.TurboJPEGSingleton" ) as mock_turbo_jpeg_singleton: mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() - for orientation in (1, 8): - stream = create_stream(hass, h264_video, {}) - stream._stream_settings.orientation = orientation + for orientation in (Orientation.NO_TRANSFORM, Orientation.ROTATE_RIGHT): + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) + stream.dynamic_stream_settings.orientation = orientation with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) diff --git a/tests/components/subaru/api_responses.py b/tests/components/subaru/api_responses.py index bd107f4bb37..315530c15b3 100644 --- a/tests/components/subaru/api_responses.py +++ b/tests/components/subaru/api_responses.py @@ -53,7 +53,6 @@ MOCK_DATETIME = datetime.fromtimestamp(1595560000, timezone.utc) VEHICLE_STATUS_EV = { "status": { "AVG_FUEL_CONSUMPTION": 2.3, - "BATTERY_VOLTAGE": 12.0, "DISTANCE_TO_EMPTY_FUEL": 707, "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", "DOOR_BOOT_POSITION": "CLOSED", @@ -75,7 +74,6 @@ VEHICLE_STATUS_EV = { "EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_PERCENT": 20, "EV_TIME_TO_FULLY_CHARGED_UTC": MOCK_DATETIME, - "EXT_EXTERNAL_TEMP": 21.5, "ODOMETER": 1234, "POSITION_HEADING_DEGREE": 150, "POSITION_SPEED_KMPH": "0", @@ -103,7 +101,7 @@ VEHICLE_STATUS_EV = { "TYRE_PRESSURE_FRONT_LEFT": 0, "TYRE_PRESSURE_FRONT_RIGHT": 2550, "TYRE_PRESSURE_REAR_LEFT": 2450, - "TYRE_PRESSURE_REAR_RIGHT": 2350, + "TYRE_PRESSURE_REAR_RIGHT": None, "TYRE_STATUS_FRONT_LEFT": "UNKNOWN", "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN", "TYRE_STATUS_REAR_LEFT": "UNKNOWN", @@ -115,9 +113,9 @@ VEHICLE_STATUS_EV = { "WINDOW_REAR_LEFT_STATUS": "UNKNOWN", "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", "WINDOW_SUNROOF_STATUS": "UNKNOWN", - "heading": 170, - "latitude": 40.0, - "longitude": -100.0, + "HEADING": 170, + "LATITUDE": 40.0, + "LONGITUDE": -100.0, } } @@ -125,7 +123,6 @@ VEHICLE_STATUS_EV = { VEHICLE_STATUS_G2 = { "status": { "AVG_FUEL_CONSUMPTION": 2.3, - "BATTERY_VOLTAGE": 12.0, "DISTANCE_TO_EMPTY_FUEL": 707, "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", "DOOR_BOOT_POSITION": "CLOSED", @@ -139,7 +136,6 @@ VEHICLE_STATUS_G2 = { "DOOR_REAR_LEFT_POSITION": "CLOSED", "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", "DOOR_REAR_RIGHT_POSITION": "CLOSED", - "EXT_EXTERNAL_TEMP": None, "ODOMETER": 1234, "POSITION_HEADING_DEGREE": 150, "POSITION_SPEED_KMPH": "0", @@ -167,7 +163,7 @@ VEHICLE_STATUS_G2 = { "TYRE_PRESSURE_FRONT_LEFT": 2550, "TYRE_PRESSURE_FRONT_RIGHT": 2550, "TYRE_PRESSURE_REAR_LEFT": 2450, - "TYRE_PRESSURE_REAR_RIGHT": 2350, + "TYRE_PRESSURE_REAR_RIGHT": None, "TYRE_STATUS_FRONT_LEFT": "UNKNOWN", "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN", "TYRE_STATUS_REAR_LEFT": "UNKNOWN", @@ -179,15 +175,14 @@ VEHICLE_STATUS_G2 = { "WINDOW_REAR_LEFT_STATUS": "UNKNOWN", "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", "WINDOW_SUNROOF_STATUS": "UNKNOWN", - "heading": 170, - "latitude": 40.0, - "longitude": -100.0, + "HEADING": 170, + "LATITUDE": 40.0, + "LONGITUDE": -100.0, } } EXPECTED_STATE_EV_IMPERIAL = { "AVG_FUEL_CONSUMPTION": "102.3", - "BATTERY_VOLTAGE": "12.0", "DISTANCE_TO_EMPTY_FUEL": "439.3", "EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", @@ -197,7 +192,6 @@ EXPECTED_STATE_EV_IMPERIAL = { "EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_PERCENT": "20", "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", - "EXT_EXTERNAL_TEMP": "70.7", "ODOMETER": "766.8", "POSITION_HEADING_DEGREE": "150", "POSITION_SPEED_KMPH": "0", @@ -207,16 +201,15 @@ EXPECTED_STATE_EV_IMPERIAL = { "TYRE_PRESSURE_FRONT_LEFT": "0.0", "TYRE_PRESSURE_FRONT_RIGHT": "37.0", "TYRE_PRESSURE_REAR_LEFT": "35.5", - "TYRE_PRESSURE_REAR_RIGHT": "34.1", + "TYRE_PRESSURE_REAR_RIGHT": "unknown", "VEHICLE_STATE_TYPE": "IGNITION_OFF", - "heading": 170, - "latitude": 40.0, - "longitude": -100.0, + "HEADING": 170, + "LATITUDE": 40.0, + "LONGITUDE": -100.0, } EXPECTED_STATE_EV_METRIC = { "AVG_FUEL_CONSUMPTION": "2.3", - "BATTERY_VOLTAGE": "12.0", "DISTANCE_TO_EMPTY_FUEL": "707", "EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", @@ -226,7 +219,6 @@ EXPECTED_STATE_EV_METRIC = { "EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_PERCENT": "20", "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", - "EXT_EXTERNAL_TEMP": "21.5", "ODOMETER": "1234", "POSITION_HEADING_DEGREE": "150", "POSITION_SPEED_KMPH": "0", @@ -236,17 +228,16 @@ EXPECTED_STATE_EV_METRIC = { "TYRE_PRESSURE_FRONT_LEFT": "0", "TYRE_PRESSURE_FRONT_RIGHT": "2550", "TYRE_PRESSURE_REAR_LEFT": "2450", - "TYRE_PRESSURE_REAR_RIGHT": "2350", + "TYRE_PRESSURE_REAR_RIGHT": "unknown", "VEHICLE_STATE_TYPE": "IGNITION_OFF", - "heading": 170, - "latitude": 40.0, - "longitude": -100.0, + "HEADING": 170, + "LATITUDE": 40.0, + "LONGITUDE": -100.0, } EXPECTED_STATE_EV_UNAVAILABLE = { "AVG_FUEL_CONSUMPTION": "unavailable", - "BATTERY_VOLTAGE": "unavailable", "DISTANCE_TO_EMPTY_FUEL": "unavailable", "EV_CHARGER_STATE_TYPE": "unavailable", "EV_CHARGE_SETTING_AMPERE_TYPE": "unavailable", @@ -256,7 +247,6 @@ EXPECTED_STATE_EV_UNAVAILABLE = { "EV_STATE_OF_CHARGE_MODE": "unavailable", "EV_STATE_OF_CHARGE_PERCENT": "unavailable", "EV_TIME_TO_FULLY_CHARGED_UTC": "unavailable", - "EXT_EXTERNAL_TEMP": "unavailable", "ODOMETER": "unavailable", "POSITION_HEADING_DEGREE": "unavailable", "POSITION_SPEED_KMPH": "unavailable", @@ -268,7 +258,7 @@ EXPECTED_STATE_EV_UNAVAILABLE = { "TYRE_PRESSURE_REAR_LEFT": "unavailable", "TYRE_PRESSURE_REAR_RIGHT": "unavailable", "VEHICLE_STATE_TYPE": "unavailable", - "heading": "unavailable", - "latitude": "unavailable", - "longitude": "unavailable", + "HEADING": "unavailable", + "LATITUDE": "unavailable", + "LONGITUDE": "unavailable", } diff --git a/tests/components/subaru/fixtures/diagnostics_config_entry.json b/tests/components/subaru/fixtures/diagnostics_config_entry.json new file mode 100644 index 00000000000..32e9ac070be --- /dev/null +++ b/tests/components/subaru/fixtures/diagnostics_config_entry.json @@ -0,0 +1,82 @@ +{ + "config_entry": { + "username": "**REDACTED**", + "password": "**REDACTED**", + "country": "USA", + "pin": "**REDACTED**", + "device_id": "**REDACTED**" + }, + "options": { + "update_enabled": true + }, + "data": [ + { + "status": { + "AVG_FUEL_CONSUMPTION": 2.3, + "DISTANCE_TO_EMPTY_FUEL": 707, + "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", + "DOOR_BOOT_POSITION": "CLOSED", + "DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN", + "DOOR_ENGINE_HOOD_POSITION": "CLOSED", + "DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_LEFT_POSITION": "CLOSED", + "DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_RIGHT_POSITION": "CLOSED", + "DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_LEFT_POSITION": "CLOSED", + "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_RIGHT_POSITION": "CLOSED", + "EV_CHARGER_STATE_TYPE": "CHARGING", + "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", + "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", + "EV_DISTANCE_TO_EMPTY": 1, + "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", + "EV_STATE_OF_CHARGE_MODE": "EV_MODE", + "EV_STATE_OF_CHARGE_PERCENT": 20, + "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", + "ODOMETER": "**REDACTED**", + "POSITION_HEADING_DEGREE": 150, + "POSITION_SPEED_KMPH": "0", + "POSITION_TIMESTAMP": 1595560000.0, + "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", + "SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED", + "SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN", + "TIMESTAMP": 1595560000.0, + "TRANSMISSION_MODE": "UNKNOWN", + "TYRE_PRESSURE_FRONT_LEFT": 0, + "TYRE_PRESSURE_FRONT_RIGHT": 2550, + "TYRE_PRESSURE_REAR_LEFT": 2450, + "TYRE_PRESSURE_REAR_RIGHT": null, + "TYRE_STATUS_FRONT_LEFT": "UNKNOWN", + "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN", + "TYRE_STATUS_REAR_LEFT": "UNKNOWN", + "TYRE_STATUS_REAR_RIGHT": "UNKNOWN", + "VEHICLE_STATE_TYPE": "IGNITION_OFF", + "WINDOW_BACK_STATUS": "UNKNOWN", + "WINDOW_FRONT_LEFT_STATUS": "VENTED", + "WINDOW_FRONT_RIGHT_STATUS": "VENTED", + "WINDOW_REAR_LEFT_STATUS": "UNKNOWN", + "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", + "WINDOW_SUNROOF_STATUS": "UNKNOWN", + "HEADING": 170, + "LATITUDE": "**REDACTED**", + "LONGITUDE": "**REDACTED**" + } + } + ] +} diff --git a/tests/components/subaru/fixtures/diagnostics_device.json b/tests/components/subaru/fixtures/diagnostics_device.json new file mode 100644 index 00000000000..c3762925d04 --- /dev/null +++ b/tests/components/subaru/fixtures/diagnostics_device.json @@ -0,0 +1,80 @@ +{ + "config_entry": { + "username": "**REDACTED**", + "password": "**REDACTED**", + "country": "USA", + "pin": "**REDACTED**", + "device_id": "**REDACTED**" + }, + "options": { + "update_enabled": true + }, + "data": { + "status": { + "AVG_FUEL_CONSUMPTION": 2.3, + "DISTANCE_TO_EMPTY_FUEL": 707, + "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", + "DOOR_BOOT_POSITION": "CLOSED", + "DOOR_ENGINE_HOOD_LOCK_STATUS": "UNKNOWN", + "DOOR_ENGINE_HOOD_POSITION": "CLOSED", + "DOOR_FRONT_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_LEFT_POSITION": "CLOSED", + "DOOR_FRONT_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_FRONT_RIGHT_POSITION": "CLOSED", + "DOOR_REAR_LEFT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_LEFT_POSITION": "CLOSED", + "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", + "DOOR_REAR_RIGHT_POSITION": "CLOSED", + "EV_CHARGER_STATE_TYPE": "CHARGING", + "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", + "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", + "EV_DISTANCE_TO_EMPTY": 1, + "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", + "EV_STATE_OF_CHARGE_MODE": "EV_MODE", + "EV_STATE_OF_CHARGE_PERCENT": 20, + "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", + "ODOMETER": "**REDACTED**", + "POSITION_HEADING_DEGREE": 150, + "POSITION_SPEED_KMPH": "0", + "POSITION_TIMESTAMP": 1595560000.0, + "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", + "SEAT_BELT_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_BELT_STATUS_FRONT_RIGHT": "BELTED", + "SEAT_BELT_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_BELT_STATUS_THIRD_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_FRONT_MIDDLE": "NOT_EQUIPPED", + "SEAT_OCCUPATION_STATUS_FRONT_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_SECOND_RIGHT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_LEFT": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_MIDDLE": "UNKNOWN", + "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN", + "TIMESTAMP": 1595560000.0, + "TRANSMISSION_MODE": "UNKNOWN", + "TYRE_PRESSURE_FRONT_LEFT": 0, + "TYRE_PRESSURE_FRONT_RIGHT": 2550, + "TYRE_PRESSURE_REAR_LEFT": 2450, + "TYRE_PRESSURE_REAR_RIGHT": null, + "TYRE_STATUS_FRONT_LEFT": "UNKNOWN", + "TYRE_STATUS_FRONT_RIGHT": "UNKNOWN", + "TYRE_STATUS_REAR_LEFT": "UNKNOWN", + "TYRE_STATUS_REAR_RIGHT": "UNKNOWN", + "VEHICLE_STATE_TYPE": "IGNITION_OFF", + "WINDOW_BACK_STATUS": "UNKNOWN", + "WINDOW_FRONT_LEFT_STATUS": "VENTED", + "WINDOW_FRONT_RIGHT_STATUS": "VENTED", + "WINDOW_REAR_LEFT_STATUS": "UNKNOWN", + "WINDOW_REAR_RIGHT_STATUS": "UNKNOWN", + "WINDOW_SUNROOF_STATUS": "UNKNOWN", + "HEADING": 170, + "LATITUDE": "**REDACTED**", + "LONGITUDE": "**REDACTED**" + } + } +} diff --git a/tests/components/subaru/test_diagnostics.py b/tests/components/subaru/test_diagnostics.py new file mode 100644 index 00000000000..547bb0edc4f --- /dev/null +++ b/tests/components/subaru/test_diagnostics.py @@ -0,0 +1,78 @@ +"""Test Subaru diagnostics.""" +import json +from unittest.mock import patch + +import pytest + +from homeassistant.components.subaru.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .api_responses import TEST_VIN_2_EV + +from tests.common import load_fixture +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) +from tests.components.subaru.conftest import ( + MOCK_API_FETCH, + MOCK_API_GET_DATA, + advance_time_to_next_fetch, +) + + +async def test_config_entry_diagnostics(hass: HomeAssistant, hass_client, ev_entry): + """Test config entry diagnostics.""" + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + diagnostics_fixture = json.loads( + load_fixture("subaru/diagnostics_config_entry.json") + ) + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == diagnostics_fixture + ) + + +async def test_device_diagnostics(hass: HomeAssistant, hass_client, ev_entry): + """Test device diagnostics.""" + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_VIN_2_EV)}, + ) + assert reg_device is not None + + diagnostics_fixture = json.loads(load_fixture("subaru/diagnostics_device.json")) + + assert ( + await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device) + == diagnostics_fixture + ) + + +async def test_device_diagnostics_vehicle_not_found( + hass: HomeAssistant, hass_client, ev_entry +): + """Test device diagnostics when the vehicle cannot be found.""" + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_VIN_2_EV)}, + ) + assert reg_device is not None + + # Simulate case where Subaru API does not return vehicle data + with patch(MOCK_API_FETCH), patch(MOCK_API_GET_DATA, return_value=None): + advance_time_to_next_fetch(hass) + await hass.async_block_till_done() + + with pytest.raises(AssertionError): + await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device) diff --git a/tests/components/switcher_kis/test_button.py b/tests/components/switcher_kis/test_button.py new file mode 100644 index 00000000000..96cc7edef37 --- /dev/null +++ b/tests/components/switcher_kis/test_button.py @@ -0,0 +1,142 @@ +"""Tests for Switcher button platform.""" +from unittest.mock import ANY, patch + +from aioswitcher.api import DeviceState, SwitcherBaseResponse, ThermostatSwing +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import slugify + +from . import init_integration +from .consts import DUMMY_THERMOSTAT_DEVICE as DEVICE + +BASE_ENTITY_ID = f"{BUTTON_DOMAIN}.{slugify(DEVICE.name)}" +ASSUME_ON_EID = BASE_ENTITY_ID + "_assume_on" +ASSUME_OFF_EID = BASE_ENTITY_ID + "_assume_off" +SWING_ON_EID = BASE_ENTITY_ID + "_vertical_swing_on" +SWING_OFF_EID = BASE_ENTITY_ID + "_vertical_swing_off" + + +@pytest.mark.parametrize( + "entity, state", + [ + (ASSUME_ON_EID, DeviceState.ON), + (ASSUME_OFF_EID, DeviceState.OFF), + ], +) +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_assume_button(hass: HomeAssistant, entity, state, mock_bridge, mock_api): + """Test assume on/off button.""" + await init_integration(hass) + assert mock_bridge + + assert hass.states.get(ASSUME_ON_EID) is not None + assert hass.states.get(ASSUME_OFF_EID) is not None + assert hass.states.get(SWING_ON_EID) is None + assert hass.states.get(SWING_OFF_EID) is None + + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity}, + blocking=True, + ) + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(ANY, state=state, update_state=True) + + +@pytest.mark.parametrize( + "entity, swing", + [ + (SWING_ON_EID, ThermostatSwing.ON), + (SWING_OFF_EID, ThermostatSwing.OFF), + ], +) +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_swing_button( + hass: HomeAssistant, entity, swing, mock_bridge, mock_api, monkeypatch +): + """Test vertical swing on/off button.""" + monkeypatch.setattr(DEVICE, "remote_id", "ELEC7022") + await init_integration(hass) + assert mock_bridge + + assert hass.states.get(ASSUME_ON_EID) is None + assert hass.states.get(ASSUME_OFF_EID) is None + assert hass.states.get(SWING_ON_EID) is not None + assert hass.states.get(SWING_OFF_EID) is not None + + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity}, + blocking=True, + ) + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(ANY, swing=swing) + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_control_device_fail(hass, mock_bridge, mock_api, monkeypatch): + """Test control device fail.""" + await init_integration(hass) + assert mock_bridge + + assert hass.states.get(ASSUME_ON_EID) is not None + + # Test exception during set hvac mode + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + side_effect=RuntimeError("fake error"), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ASSUME_ON_EID}, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with( + ANY, state=DeviceState.ON, update_state=True + ) + + state = hass.states.get(ASSUME_ON_EID) + assert state.state == STATE_UNAVAILABLE + + # Make device available again + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert hass.states.get(ASSUME_ON_EID) is not None + + # Test error response during turn on + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + return_value=SwitcherBaseResponse(None), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ASSUME_ON_EID}, + blocking=True, + ) + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once_with( + ANY, state=DeviceState.ON, update_state=True + ) + + state = hass.states.get(ASSUME_ON_EID) + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 015c3a2ab16..25259ac7ee9 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -281,6 +281,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): "source": SOURCE_REAUTH, "entry_id": entry.entry_id, "unique_id": entry.unique_id, + "title_placeholders": {"name": entry.title}, }, data={ CONF_HOST: HOST, @@ -409,7 +410,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL - assert result["title"] == "192.168.1.5" + assert result["title"] == "mydsm" assert result["data"][CONF_HOST] == "192.168.1.5" assert result["data"][CONF_PORT] == 5001 assert result["data"][CONF_SSL] == DEFAULT_USE_SSL diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index ba233d04a78..0872060712e 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -16,7 +16,7 @@ from homeassistant.components.tellduslive import ( from homeassistant.config_entries import SOURCE_DISCOVERY from homeassistant.const import CONF_HOST -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry def init_config_flow(hass, side_effect=None): diff --git a/tests/components/text/__init__.py b/tests/components/text/__init__.py new file mode 100644 index 00000000000..e22fa1d34a1 --- /dev/null +++ b/tests/components/text/__init__.py @@ -0,0 +1 @@ +"""Tests for the text component.""" diff --git a/tests/components/text/test_device_action.py b/tests/components/text/test_device_action.py new file mode 100644 index 00000000000..abddb5092c4 --- /dev/null +++ b/tests/components/text/test_device_action.py @@ -0,0 +1,185 @@ +"""The tests for Text device actions.""" +import pytest +import voluptuous_serialize + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.text import DOMAIN, device_action +from homeassistant.helpers import config_validation as cv, device_registry +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_registry import RegistryEntryHider +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: F401 + + +@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("text.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": "text.test_5678", + "metadata": {"secondary": False}, + }, + ] + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) + assert_lists_same(actions, expected_actions) + + +@pytest.mark.parametrize( + "hidden_by,entity_category", + ( + (RegistryEntryHider.INTEGRATION, None), + (RegistryEntryHider.USER, None), + (None, EntityCategory.CONFIG), + (None, EntityCategory.DIAGNOSTIC), + ), +) +async def test_get_actions_hidden_auxiliary( + hass, + device_reg, + entity_reg, + hidden_by, + entity_category, +): + """Test we get the expected actions from a hidden or auxiliary 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, + entity_category=entity_category, + hidden_by=hidden_by, + ) + expected_actions = [] + expected_actions += [ + { + "domain": DOMAIN, + "type": action, + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + "metadata": {"secondary": True}, + } + for action in ["set_value"] + ] + actions = await async_get_device_automations( + hass, DeviceAutomationType.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": "text.test_5678", + "metadata": {"secondary": False}, + }, + ] + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for actions.""" + hass.states.async_set("text.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": "text.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": "text.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": "string"}] diff --git a/tests/components/text/test_init.py b/tests/components/text/test_init.py new file mode 100644 index 00000000000..dda746fe6a5 --- /dev/null +++ b/tests/components/text/test_init.py @@ -0,0 +1,197 @@ +"""The tests for the text component.""" +import pytest + +from homeassistant.components.text import ( + ATTR_MAX, + ATTR_MIN, + ATTR_MODE, + ATTR_PATTERN, + ATTR_VALUE, + DOMAIN, + SERVICE_SET_VALUE, + TextEntity, + TextMode, + _async_set_value, +) +from homeassistant.const import MAX_LENGTH_STATE_STATE +from homeassistant.core import ServiceCall, State +from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY +from homeassistant.setup import async_setup_component + +from tests.common import mock_restore_cache_with_extra_data + + +class MockTextEntity(TextEntity): + """Mock text device to use in tests.""" + + def __init__( + self, native_value="test", native_min=None, native_max=None, pattern=None + ): + """Initialize mock text entity.""" + self._attr_native_value = native_value + if native_min is not None: + self._attr_native_min = native_min + if native_max is not None: + self._attr_native_max = native_max + if pattern is not None: + self._attr_pattern = pattern + + async def async_set_value(self, value: str) -> None: + """Set the value of the text.""" + self._attr_native_value = value + + +async def test_text_default(hass): + """Test text entity with defaults.""" + text = MockTextEntity() + text.hass = hass + + assert text.capability_attributes == { + ATTR_MIN: 0, + ATTR_MAX: MAX_LENGTH_STATE_STATE, + ATTR_MODE: TextMode.TEXT, + ATTR_PATTERN: None, + } + assert text.pattern is None + assert text.state == "test" + + +async def test_text_new_min_max_pattern(hass): + """Test text entity with new min, max, and pattern.""" + text = MockTextEntity(native_min=-1, native_max=500, pattern=r"[a-z]") + text.hass = hass + + assert text.capability_attributes == { + ATTR_MIN: 0, + ATTR_MAX: MAX_LENGTH_STATE_STATE, + ATTR_MODE: TextMode.TEXT, + ATTR_PATTERN: r"[a-z]", + } + + +async def test_text_set_value(hass): + """Test text entity with set_value service.""" + text = MockTextEntity(native_min=1, native_max=5, pattern=r"[a-z]") + text.hass = hass + + with pytest.raises(ValueError): + await _async_set_value( + text, ServiceCall(DOMAIN, SERVICE_SET_VALUE, {ATTR_VALUE: ""}) + ) + + with pytest.raises(ValueError): + await _async_set_value( + text, ServiceCall(DOMAIN, SERVICE_SET_VALUE, {ATTR_VALUE: "hello world!"}) + ) + + with pytest.raises(ValueError): + await _async_set_value( + text, ServiceCall(DOMAIN, SERVICE_SET_VALUE, {ATTR_VALUE: "HELLO"}) + ) + + await _async_set_value( + text, ServiceCall(DOMAIN, SERVICE_SET_VALUE, {ATTR_VALUE: "test2"}) + ) + + assert text.state == "test2" + + +async def test_text_value_outside_bounds(hass): + """Test text entity with value that is outside min and max.""" + with pytest.raises(ValueError): + _ = MockTextEntity( + "hello world", native_min=2, native_max=5, pattern=r"[a-z]" + ).state + with pytest.raises(ValueError): + _ = MockTextEntity( + "hello world", native_min=15, native_max=20, pattern=r"[a-z]" + ).state + + +RESTORE_DATA = { + "native_max": 5, + "native_min": 1, + # "mode": TextMode.TEXT, + # "pattern": r"[A-Za-z0-9]", + "native_value": "Hello", +} + + +async def test_restore_number_save_state( + hass, + hass_storage, + enable_custom_integrations, +): + """Test RestoreNumber.""" + platform = getattr(hass.components, "test.text") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockRestoreText( + name="Test", + native_max=5, + native_min=1, + native_value="Hello", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "text", {"text": {"platform": "test"}}) + await hass.async_block_till_done() + + # Trigger saving state + await hass.async_stop() + + assert len(hass_storage[RESTORE_STATE_KEY]["data"]) == 1 + state = hass_storage[RESTORE_STATE_KEY]["data"][0]["state"] + assert state["entity_id"] == entity0.entity_id + extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] + assert extra_data == RESTORE_DATA + assert isinstance(extra_data["native_value"], str) + + +@pytest.mark.parametrize( + "native_max, native_min, native_value, native_value_type, extra_data", + [ + (5, 1, "Hello", str, RESTORE_DATA), + (255, 1, None, type(None), None), + (255, 1, None, type(None), {}), + (255, 1, None, type(None), {"beer": 123}), + (255, 1, None, type(None), {"native_value": {}}), + ], +) +async def test_restore_number_restore_state( + hass, + enable_custom_integrations, + hass_storage, + native_max, + native_min, + native_value, + native_value_type, + extra_data, +): + """Test RestoreNumber.""" + mock_restore_cache_with_extra_data(hass, ((State("text.test", ""), extra_data),)) + + platform = getattr(hass.components, "test.text") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockRestoreText( + native_max=native_max, + native_min=native_min, + name="Test", + native_value=None, + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "text", {"text": {"platform": "test"}}) + await hass.async_block_till_done() + + assert hass.states.get(entity0.entity_id) + + assert entity0.native_max == native_max + assert entity0.native_min == native_min + assert entity0.mode == TextMode.TEXT + assert entity0.pattern is None + assert entity0.native_value == native_value + assert isinstance(entity0.native_value, native_value_type) diff --git a/tests/components/text/test_recorder.py b/tests/components/text/test_recorder.py new file mode 100644 index 00000000000..6ad0df13ffa --- /dev/null +++ b/tests/components/text/test_recorder.py @@ -0,0 +1,41 @@ +"""The tests for text recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import text +from homeassistant.components.recorder.db_schema import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.components.text import ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_PATTERN +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done + + +async def test_exclude_attributes(recorder_mock, hass): + """Test siren registered attributes to be excluded.""" + await async_setup_component(hass, text.DOMAIN, {text.DOMAIN: {"platform": "demo"}}) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + for attr in (ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_PATTERN): + assert attr not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/text/test_reproduce_state.py b/tests/components/text/test_reproduce_state.py new file mode 100644 index 00000000000..fd2bd7b7c90 --- /dev/null +++ b/tests/components/text/test_reproduce_state.py @@ -0,0 +1,53 @@ +"""Test reproduce state for Text entities.""" +from homeassistant.components.text.const import ( + ATTR_MAX, + ATTR_MIN, + ATTR_MODE, + ATTR_PATTERN, + DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state + +from tests.common import async_mock_service + +VALID_TEXT1 = "Hello" +VALID_TEXT2 = "World" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Text states.""" + + hass.states.async_set( + "text.test_text", + VALID_TEXT1, + {ATTR_MIN: 1, ATTR_MAX: 5, ATTR_MODE: "text", ATTR_PATTERN: None}, + ) + + # These calls should do nothing as entities already in desired state + await async_reproduce_state( + hass, + [ + State("text.test_text", VALID_TEXT1), + # Should not raise + State("text.non_existing", "234"), + ], + ) + + assert hass.states.get("text.test_text").state == VALID_TEXT1 + + # Test reproducing with different state + calls = async_mock_service(hass, DOMAIN, SERVICE_SET_VALUE) + await async_reproduce_state( + hass, + [ + State("text.test_text", VALID_TEXT2), + # Should not raise + State("text.non_existing", "234"), + ], + ) + + assert len(calls) == 1 + assert calls[0].domain == DOMAIN + assert calls[0].data == {"entity_id": "text.test_text", "value": VALID_TEXT2} diff --git a/tests/components/tibber/test_statistics.py b/tests/components/tibber/test_statistics.py index 745c434237b..661297f9a37 100644 --- a/tests/components/tibber/test_statistics.py +++ b/tests/components/tibber/test_statistics.py @@ -35,7 +35,8 @@ async def test_async_setup_entry(recorder_mock, hass): None, [statistic_id], "hour", - True, + None, + {"start", "state", "mean", "min", "max", "last_reset", "sum"}, ) assert len(stats) == 1 diff --git a/tests/components/tilt_ble/test_config_flow.py b/tests/components/tilt_ble/test_config_flow.py index c2bb20e0dd6..789bb86e30c 100644 --- a/tests/components/tilt_ble/test_config_flow.py +++ b/tests/components/tilt_ble/test_config_flow.py @@ -21,7 +21,7 @@ async def test_async_step_bluetooth_valid_device(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( - "homeassistant.components.govee_ble.async_setup_entry", return_value=True + "homeassistant.components.tilt_ble.async_setup_entry", return_value=True ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index b0e5fc1f028..c0444b02eb8 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -5,6 +5,8 @@ from freezegun import freeze_time import pytest from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_next from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -786,3 +788,25 @@ async def test_simple_before_after_does_not_loop_berlin_in_range(hass): assert state.attributes["after"] == "2019-01-11T00:00:00+01:00" assert state.attributes["before"] == "2019-01-11T06:00:00+01:00" assert state.attributes["next_update"] == "2019-01-11T06:00:00+01:00" + + +async def test_unique_id(hass: HomeAssistant) -> None: + """Test unique id.""" + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Evening", + "after": "18:00", + "before": "22:00", + "unique_id": "very_unique_id", + } + ] + } + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + entity_reg = er.async_get(hass) + entity = entity_reg.async_get("binary_sensor.evening") + + assert entity.unique_id == "very_unique_id" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 830670efc11..4c6c235a3aa 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -24,7 +24,7 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture(name="client") -async def traccar_client(loop, hass, hass_client_no_auth): +async def traccar_client(event_loop, hass, hass_client_no_auth): """Mock client for Traccar (unauthenticated).""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -36,7 +36,7 @@ async def traccar_client(loop, hass, hass_client_no_auth): @pytest.fixture(autouse=True) -async def setup_zones(loop, hass): +async def setup_zones(event_loop, hass): """Set up Zone config in HA.""" assert await async_setup_component( hass, diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py index 11f8a1abd75..79c24e5970d 100644 --- a/tests/components/twentemilieu/test_calendar.py +++ b/tests/components/twentemilieu/test_calendar.py @@ -81,4 +81,7 @@ async def test_api_events( "summary": "Christmas tree pickup", "description": None, "location": None, + "uid": None, + "recurrence_id": None, + "rrule": None, } diff --git a/tests/components/twinkly/__init__.py b/tests/components/twinkly/__init__.py index d5440ddb74a..31d1eff2a61 100644 --- a/tests/components/twinkly/__init__.py +++ b/tests/components/twinkly/__init__.py @@ -22,6 +22,11 @@ class ClientMock: self.state = True self.brightness = {"mode": "enabled", "value": 10} self.color = None + self.movies = [{"id": 1, "name": "Rainbow"}, {"id": 2, "name": "Flare"}] + self.current_movie = {} + self.default_mode = "movie" + self.mode = None + self.version = "2.8.10" self.id = str(uuid4()) self.device_info = { @@ -52,6 +57,7 @@ class ClientMock: if self.is_offline: raise ClientConnectionError() self.state = True + self.mode = self.default_mode async def turn_off(self) -> None: """Set the mocked off state.""" @@ -78,6 +84,36 @@ class ClientMock: async def set_static_colour(self, colour) -> None: """Set static color.""" self.color = colour + self.default_mode = "color" + + async def set_cycle_colours(self, colour) -> None: + """Set static color.""" + self.color = colour + self.default_mode = "movie" async def interview(self) -> None: """Interview.""" + + async def get_saved_movies(self) -> dict: + """Get saved movies.""" + return self.movies + + async def get_current_movie(self) -> dict: + """Get current movie.""" + return self.current_movie + + async def set_current_movie(self, movie_id: int) -> dict: + """Set current movie.""" + self.current_movie = {"id": movie_id} + + async def set_mode(self, mode: str) -> None: + """Set mode.""" + if mode == "off": + await self.turn_off() + else: + await self.turn_on() + self.mode = mode + + async def get_firmware_version(self) -> dict: + """Get firmware version.""" + return {"version": self.version} diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 40fea31a6ba..53e589c564b 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import patch -from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.components.light import ATTR_BRIGHTNESS, LightEntityFeature from homeassistant.components.twinkly.const import ( CONF_HOST, CONF_ID, @@ -55,9 +55,8 @@ async def test_turn_on_off(hass: HomeAssistant): assert hass.states.get(entity.entity_id).state == "off" await hass.services.async_call( - "light", "turn_on", service_data={"entity_id": entity.entity_id} + "light", "turn_on", service_data={"entity_id": entity.entity_id}, blocking=True ) - await hass.async_block_till_done() state = hass.states.get(entity.entity_id) @@ -78,8 +77,8 @@ async def test_turn_on_with_brightness(hass: HomeAssistant): "light", "turn_on", service_data={"entity_id": entity.entity_id, "brightness": 255}, + blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(entity.entity_id) @@ -90,8 +89,8 @@ async def test_turn_on_with_brightness(hass: HomeAssistant): "light", "turn_on", service_data={"entity_id": entity.entity_id, "brightness": 1}, + blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(entity.entity_id) @@ -99,7 +98,7 @@ async def test_turn_on_with_brightness(hass: HomeAssistant): async def test_turn_on_with_color_rgbw(hass: HomeAssistant): - """Test support of the light.turn_on service with a brightness parameter.""" + """Test support of the light.turn_on service with a rgbw parameter.""" client = ClientMock() client.state = False client.device_info["led_profile"] = "RGBW" @@ -107,22 +106,28 @@ async def test_turn_on_with_color_rgbw(hass: HomeAssistant): entity, _, _, _ = await _create_entries(hass, client) assert hass.states.get(entity.entity_id).state == "off" + assert ( + LightEntityFeature.EFFECT + & hass.states.get(entity.entity_id).attributes["supported_features"] + ) await hass.services.async_call( "light", "turn_on", service_data={"entity_id": entity.entity_id, "rgbw_color": (128, 64, 32, 0)}, + blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(entity.entity_id) assert state.state == "on" - assert client.color == (0, 128, 64, 32) + assert client.color == (128, 64, 32) + assert client.default_mode == "color" + assert client.mode == "color" async def test_turn_on_with_color_rgb(hass: HomeAssistant): - """Test support of the light.turn_on service with a brightness parameter.""" + """Test support of the light.turn_on service with a rgb parameter.""" client = ClientMock() client.state = False client.device_info["led_profile"] = "RGB" @@ -130,18 +135,145 @@ async def test_turn_on_with_color_rgb(hass: HomeAssistant): entity, _, _, _ = await _create_entries(hass, client) assert hass.states.get(entity.entity_id).state == "off" + assert ( + LightEntityFeature.EFFECT + & hass.states.get(entity.entity_id).attributes["supported_features"] + ) await hass.services.async_call( "light", "turn_on", service_data={"entity_id": entity.entity_id, "rgb_color": (128, 64, 32)}, + blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(entity.entity_id) assert state.state == "on" assert client.color == (128, 64, 32) + assert client.default_mode == "color" + assert client.mode == "color" + + +async def test_turn_on_with_effect(hass: HomeAssistant): + """Test support of the light.turn_on service with effects.""" + client = ClientMock() + client.state = False + client.device_info["led_profile"] = "RGB" + client.brightness = {"mode": "enabled", "value": 255} + entity, _, _, _ = await _create_entries(hass, client) + + assert hass.states.get(entity.entity_id).state == "off" + assert not client.current_movie + assert ( + LightEntityFeature.EFFECT + & hass.states.get(entity.entity_id).attributes["supported_features"] + ) + + await hass.services.async_call( + "light", + "turn_on", + service_data={"entity_id": entity.entity_id, "effect": "1 Rainbow"}, + blocking=True, + ) + + state = hass.states.get(entity.entity_id) + + assert state.state == "on" + assert client.current_movie["id"] == 1 + assert client.default_mode == "movie" + assert client.mode == "movie" + + +async def test_turn_on_with_color_rgbw_and_missing_effect(hass: HomeAssistant): + """Test support of the light.turn_on service with rgbw color and missing effect support.""" + client = ClientMock() + client.state = False + client.device_info["led_profile"] = "RGBW" + client.brightness = {"mode": "enabled", "value": 255} + client.version = "2.7.0" + entity, _, _, _ = await _create_entries(hass, client) + + assert hass.states.get(entity.entity_id).state == "off" + assert ( + not LightEntityFeature.EFFECT + & hass.states.get(entity.entity_id).attributes["supported_features"] + ) + + await hass.services.async_call( + "light", + "turn_on", + service_data={"entity_id": entity.entity_id, "rgbw_color": (128, 64, 32, 0)}, + blocking=True, + ) + + state = hass.states.get(entity.entity_id) + + assert state.state == "on" + assert client.color == (0, 128, 64, 32) + assert client.mode == "movie" + assert client.default_mode == "movie" + + +async def test_turn_on_with_color_rgb_and_missing_effect(hass: HomeAssistant): + """Test support of the light.turn_on service with rgb color and missing effect support.""" + client = ClientMock() + client.state = False + client.device_info["led_profile"] = "RGB" + client.brightness = {"mode": "enabled", "value": 255} + client.version = "2.7.0" + entity, _, _, _ = await _create_entries(hass, client) + + assert hass.states.get(entity.entity_id).state == "off" + assert ( + not LightEntityFeature.EFFECT + & hass.states.get(entity.entity_id).attributes["supported_features"] + ) + + await hass.services.async_call( + "light", + "turn_on", + service_data={"entity_id": entity.entity_id, "rgb_color": (128, 64, 32)}, + blocking=True, + ) + + state = hass.states.get(entity.entity_id) + + assert state.state == "on" + assert client.color == (128, 64, 32) + assert client.mode == "movie" + assert client.default_mode == "movie" + + +async def test_turn_on_with_effect_missing_effects(hass: HomeAssistant): + """Test support of the light.turn_on service with effect set even if effects are not supported.""" + client = ClientMock() + client.state = False + client.device_info["led_profile"] = "RGB" + client.brightness = {"mode": "enabled", "value": 255} + client.version = "2.7.0" + entity, _, _, _ = await _create_entries(hass, client) + + assert hass.states.get(entity.entity_id).state == "off" + assert not client.current_movie + assert ( + not LightEntityFeature.EFFECT + & hass.states.get(entity.entity_id).attributes["supported_features"] + ) + + await hass.services.async_call( + "light", + "turn_on", + service_data={"entity_id": entity.entity_id, "effect": "1 Rainbow"}, + blocking=True, + ) + + state = hass.states.get(entity.entity_id) + + assert state.state == "on" + assert not client.current_movie + assert client.default_mode == "movie" + assert client.mode == "movie" async def test_turn_off(hass: HomeAssistant): @@ -151,9 +283,8 @@ async def test_turn_off(hass: HomeAssistant): assert hass.states.get(entity.entity_id).state == "on" await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity.entity_id} + "light", "turn_off", service_data={"entity_id": entity.entity_id}, blocking=True ) - await hass.async_block_till_done() state = hass.states.get(entity.entity_id) @@ -172,9 +303,8 @@ async def test_update_name(hass: HomeAssistant): client.change_name("new_device_name") await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity.entity_id} + "light", "turn_off", service_data={"entity_id": entity.entity_id}, blocking=True ) # We call turn_off which will automatically cause an async_update - await hass.async_block_till_done() state = hass.states.get(entity.entity_id) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index a1f6f3d4b02..078c068c8ed 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -16,7 +16,6 @@ from homeassistant.components.unifi.const import ( CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, - CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, @@ -473,7 +472,6 @@ async def test_advanced_option_flow(hass, aioclient_mock): result["flow_id"], user_input={ CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], - CONF_POE_CLIENTS: False, CONF_DPI_RESTRICTIONS: False, }, ) @@ -498,7 +496,6 @@ async def test_advanced_option_flow(hass, aioclient_mock): CONF_SSID_FILTER: ["SSID 1", "SSID 2_IOT", "SSID 3"], CONF_DETECTION_TIME: 100, CONF_IGNORE_WIRED_BUG: False, - CONF_POE_CLIENTS: False, CONF_DPI_RESTRICTIONS: False, CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], CONF_ALLOW_BANDWIDTH_SENSORS: True, diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 6584b947293..9de0e4b6154 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -6,16 +6,6 @@ from homeassistant.components.unifi.const import ( CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, ) -from homeassistant.components.unifi.device_tracker import CLIENT_TRACKER, DEVICE_TRACKER -from homeassistant.components.unifi.sensor import RX_SENSOR, TX_SENSOR, UPTIME_SENSOR -from homeassistant.components.unifi.switch import ( - BLOCK_SWITCH, - DPI_SWITCH, - OUTLET_SWITCH, - POE_SWITCH, -) -from homeassistant.components.unifi.update import DEVICE_UPDATE -from homeassistant.const import Platform from .test_controller import setup_unifi_integration @@ -146,26 +136,6 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): "version": 1, }, "site_role": "admin", - "entities": { - str(Platform.DEVICE_TRACKER): { - CLIENT_TRACKER: ["00:00:00:00:00:00"], - DEVICE_TRACKER: ["00:00:00:00:00:01"], - }, - str(Platform.SENSOR): { - RX_SENSOR: ["00:00:00:00:00:00"], - TX_SENSOR: ["00:00:00:00:00:00"], - UPTIME_SENSOR: ["00:00:00:00:00:00"], - }, - str(Platform.SWITCH): { - BLOCK_SWITCH: ["00:00:00:00:00:00"], - DPI_SWITCH: ["5f976f4ae3c58f018ec7dff6"], - POE_SWITCH: ["00:00:00:00:00:00"], - OUTLET_SWITCH: [], - }, - str(Platform.UPDATE): { - DEVICE_UPDATE: ["00:00:00:00:00:01"], - }, - }, "clients": { "00:00:00:00:00:00": { "blocked": False, diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 03ea89097c5..1362f1dc0d5 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,19 +2,13 @@ from unittest.mock import patch from homeassistant.components import unifi -from homeassistant.components.unifi import async_flatten_entry_data -from homeassistant.components.unifi.const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN +from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.setup import async_setup_component -from .test_controller import ( - CONTROLLER_DATA, - DEFAULT_CONFIG_ENTRY_ID, - ENTRY_CONFIG, - setup_unifi_integration, -) +from .test_controller import DEFAULT_CONFIG_ENTRY_ID, setup_unifi_integration -from tests.common import MockConfigEntry, flush_store +from tests.common import flush_store async def test_setup_with_no_config(hass): @@ -52,17 +46,6 @@ async def test_setup_entry_fails_trigger_reauth_flow(hass): assert hass.data[UNIFI_DOMAIN] == {} -async def test_flatten_entry_data(hass): - """Verify entry data can be flattened.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data={CONF_CONTROLLER: CONTROLLER_DATA}, - ) - await async_flatten_entry_data(hass, entry) - - assert entry.data == ENTRY_CONFIG - - async def test_unload_entry(hass, aioclient_mock): """Test being able to unload an entry.""" config_entry = await setup_unifi_integration(hass, aioclient_mock) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index e6357b03172..5178e3dda37 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -6,7 +6,7 @@ from datetime import timedelta from aiounifi.models.message import MessageKey from aiounifi.websocket import WebsocketState -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -16,12 +16,10 @@ from homeassistant.components.switch import ( from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, CONF_DPI_RESTRICTIONS, - CONF_POE_CLIENTS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.components.unifi.switch import POE_SWITCH from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -38,13 +36,12 @@ from homeassistant.util import dt from .test_controller import ( CONTROLLER_HOST, - DEFAULT_CONFIG_ENTRY_ID, DESCRIPTION, ENTRY_CONFIG, setup_unifi_integration, ) -from tests.common import async_fire_time_changed, mock_restore_cache +from tests.common import async_fire_time_changed CLIENT_1 = { "hostname": "client_1", @@ -636,23 +633,14 @@ async def test_switches(hass, aioclient_mock): CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, }, - clients_response=[CLIENT_1, CLIENT_4], - devices_response=[DEVICE_1], + clients_response=[CLIENT_4], clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1], dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 - - switch_1 = hass.states.get("switch.poe_client_1") - assert switch_1 is not None - assert switch_1.state == "on" - assert switch_1.attributes["power"] == "2.56" - assert switch_1.attributes[SWITCH_DOMAIN] == "10:00:00:00:01:01" - assert switch_1.attributes["port"] == 1 - assert switch_1.attributes["poe_mode"] == "auto" + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 switch_4 = hass.states.get("switch.poe_client_4") assert switch_4 is None @@ -671,11 +659,7 @@ async def test_switches(hass, aioclient_mock): assert dpi_switch.attributes["icon"] == "mdi:network" ent_reg = er.async_get(hass) - for entry_id in ( - "switch.poe_client_1", - "switch.block_client_1", - "switch.block_media_streaming", - ): + for entry_id in ("switch.block_client_1", "switch.block_media_streaming"): assert ent_reg.async_get(entry_id).entity_category is EntityCategory.CONFIG # Block and unblock client @@ -729,7 +713,7 @@ async def test_switches(hass, aioclient_mock): # Make sure no duplicates arise on generic signal update async_dispatcher_send(hass, controller.signal_update) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket): @@ -738,24 +722,21 @@ async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket): hass, aioclient_mock, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, - clients_response=[CLIENT_1, UNBLOCKED], - devices_response=[DEVICE_1], + clients_response=[UNBLOCKED], dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - assert hass.states.get("switch.poe_client_1") is not None assert hass.states.get("switch.block_client_2") is not None assert hass.states.get("switch.block_media_streaming") is not None - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[CLIENT_1, UNBLOCKED]) + mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[UNBLOCKED]) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - assert hass.states.get("switch.poe_client_1") is None assert hass.states.get("switch.block_client_2") is None assert hass.states.get("switch.block_media_streaming") is not None @@ -1089,273 +1070,20 @@ async def test_option_remove_switches(hass, aioclient_mock): CONF_TRACK_DEVICES: False, }, clients_response=[CLIENT_1], - devices_response=[DEVICE_1], dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Disable DPI Switches hass.config_entries.async_update_entry( config_entry, - options={CONF_DPI_RESTRICTIONS: False, CONF_POE_CLIENTS: False}, + options={CONF_DPI_RESTRICTIONS: False}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_new_client_discovered_on_poe_control( - hass, aioclient_mock, mock_unifi_websocket -): - """Test if 2nd update has a new client.""" - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, - clients_response=[CLIENT_1], - devices_response=[DEVICE_1], - ) - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - mock_unifi_websocket(message=MessageKey.CLIENT, data=CLIENT_2) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_CLIENT_2_CONNECTED) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - switch_2 = hass.states.get("switch.poe_client_2") - assert switch_2 is not None - - aioclient_mock.put( - f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/mock-id", - ) - - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True - ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - assert aioclient_mock.call_count == 11 - assert aioclient_mock.mock_calls[10][2] == { - "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] - } - - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True - ) - assert aioclient_mock.call_count == 12 - assert aioclient_mock.mock_calls[11][2] == { - "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"}] - } - - -async def test_ignore_multiple_poe_clients_on_same_port(hass, aioclient_mock): - """Ignore when there are multiple POE driven clients on same port. - - If there is a non-UniFi switch powered by POE, - clients will be transparently marked as having POE as well. - """ - await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=POE_SWITCH_CLIENTS, - devices_response=[DEVICE_1], - ) - - switch_1 = hass.states.get("switch.poe_client_1") - switch_2 = hass.states.get("switch.poe_client_2") - assert switch_1 is None - assert switch_2 is None - - -async def test_restore_client_succeed(hass, aioclient_mock): - """Test that RestoreEntity works as expected.""" - POE_DEVICE = { - "device_id": "12345", - "ip": "1.0.1.1", - "mac": "00:00:00:00:01:01", - "last_seen": 1562600145, - "model": "US16P150", - "name": "POE Switch", - "port_overrides": [ - { - "poe_mode": "off", - "port_idx": 1, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - } - ], - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "op_mode": "switch", - "poe_caps": 7, - "poe_class": "Unknown", - "poe_current": "0.00", - "poe_enable": False, - "poe_good": False, - "poe_mode": "off", - "poe_power": "0.00", - "poe_voltage": "0.00", - "port_idx": 1, - "port_poe": True, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - "up": False, - }, - ], - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - POE_CLIENT = { - "hostname": "poe_client", - "ip": "1.0.0.1", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - "name": "POE Client", - "oui": "Producer", - } - - fake_state = core.State( - "switch.poe_client", - "off", - { - "power": "0.00", - "switch": POE_DEVICE["mac"], - "port": 1, - "poe_mode": "auto", - }, - ) - mock_restore_cache(hass, (fake_state,)) - - config_entry = config_entries.ConfigEntry( - version=1, - domain=UNIFI_DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - options={}, - entry_id=DEFAULT_CONFIG_ENTRY_ID, - ) - - registry = er.async_get(hass) - registry.async_get_or_create( - SWITCH_DOMAIN, - UNIFI_DOMAIN, - f'{POE_SWITCH}-{POE_CLIENT["mac"]}', - suggested_object_id=POE_CLIENT["hostname"], - config_entry=config_entry, - ) - - await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, - clients_response=[], - devices_response=[POE_DEVICE], - clients_all_response=[POE_CLIENT], - ) - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - poe_client = hass.states.get("switch.poe_client") - assert poe_client.state == "off" - - -async def test_restore_client_no_old_state(hass, aioclient_mock): - """Test that RestoreEntity without old state makes entity unavailable.""" - POE_DEVICE = { - "device_id": "12345", - "ip": "1.0.1.1", - "mac": "00:00:00:00:01:01", - "last_seen": 1562600145, - "model": "US16P150", - "name": "POE Switch", - "port_overrides": [ - { - "poe_mode": "off", - "port_idx": 1, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - } - ], - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "op_mode": "switch", - "poe_caps": 7, - "poe_class": "Unknown", - "poe_current": "0.00", - "poe_enable": False, - "poe_good": False, - "poe_mode": "off", - "poe_power": "0.00", - "poe_voltage": "0.00", - "port_idx": 1, - "port_poe": True, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - "up": False, - }, - ], - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - POE_CLIENT = { - "hostname": "poe_client", - "ip": "1.0.0.1", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - "name": "POE Client", - "oui": "Producer", - } - - config_entry = config_entries.ConfigEntry( - version=1, - domain=UNIFI_DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - options={}, - entry_id=DEFAULT_CONFIG_ENTRY_ID, - ) - - registry = er.async_get(hass) - registry.async_get_or_create( - SWITCH_DOMAIN, - UNIFI_DOMAIN, - f'{POE_SWITCH}-{POE_CLIENT["mac"]}', - suggested_object_id=POE_CLIENT["hostname"], - config_entry=config_entry, - ) - - await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, - clients_response=[], - devices_response=[POE_DEVICE], - clients_all_response=[POE_CLIENT], - ) - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - poe_client = hass.states.get("switch.poe_client") - assert poe_client.state == "unavailable" # self.poe_mode is None - - async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket): """Test the update_items function with some clients.""" config_entry = await setup_unifi_integration( @@ -1447,3 +1175,33 @@ async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket): mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF + + +async def test_remove_poe_client_switches(hass, aioclient_mock): + """Test old PoE client switches are removed.""" + + config_entry = config_entries.ConfigEntry( + version=1, + domain=UNIFI_DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + options={}, + entry_id="1", + ) + + ent_reg = er.async_get(hass) + ent_reg.async_get_or_create( + SWITCH_DOMAIN, + UNIFI_DOMAIN, + "poe-123", + config_entry=config_entry, + ) + + await setup_unifi_integration(hass, aioclient_mock) + + assert not [ + entry + for entry in ent_reg.entities.values() + if entry.config_entry_id == config_entry.entry_id + ] diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index b006dfbd004..77aa9622f9e 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -128,7 +128,7 @@ def mock_entry( """Mock ProtectApiClient for testing.""" with _patch_discovery(no_device=True), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" + "homeassistant.components.unifiprotect.utils.ProtectApiClient" ) as mock_api: ufp_config_entry.add_to_hass(hass) diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index b2eec518d40..152628c75f9 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -11,8 +11,8 @@ from pyunifiprotect.data.nvr import EventMetadata from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.unifiprotect.binary_sensor import ( CAMERA_SENSORS, + EVENT_SENSORS, LIGHT_SENSORS, - MOTION_SENSORS, SENSE_SENSORS, ) from homeassistant.components.unifiprotect.const import ( @@ -50,11 +50,11 @@ async def test_binary_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7) async def test_binary_sensor_light_remove( @@ -120,11 +120,11 @@ async def test_binary_sensor_setup_camera_all( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7) entity_registry = er.async_get(hass) - description = CAMERA_SENSORS[0] + description = EVENT_SENSORS[0] unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, doorbell, description ) @@ -139,7 +139,7 @@ async def test_binary_sensor_setup_camera_all( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION # Is Dark - description = CAMERA_SENSORS[1] + description = CAMERA_SENSORS[0] unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, doorbell, description ) @@ -154,7 +154,7 @@ async def test_binary_sensor_setup_camera_all( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION # Motion - description = MOTION_SENSORS[0] + description = EVENT_SENSORS[1] unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, doorbell, description ) @@ -167,7 +167,6 @@ async def test_binary_sensor_setup_camera_all( assert state assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - assert state.attributes[ATTR_EVENT_SCORE] == 0 async def test_binary_sensor_setup_camera_none( @@ -180,7 +179,7 @@ async def test_binary_sensor_setup_camera_none( assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) entity_registry = er.async_get(hass) - description = CAMERA_SENSORS[1] + description = CAMERA_SENSORS[0] unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, camera, description @@ -263,10 +262,10 @@ async def test_binary_sensor_update_motion( """Test binary_sensor motion entity.""" await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 9) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 13, 13) _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, doorbell, MOTION_SENSORS[0] + Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[1] ) event = Event( diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index d0fb0dba9f2..1ee1d40515b 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -246,7 +246,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - mock_config.add_to_hass(hass) with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" + "homeassistant.components.unifiprotect.utils.ProtectApiClient" ) as mock_api: mock_api.return_value = ufp_client @@ -254,23 +254,29 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - await hass.async_block_till_done() assert mock_config.state == config_entries.ConfigEntryState.LOADED - result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(mock_config.entry_id) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + assert result["step_id"] == "init" - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - {CONF_DISABLE_RTSP: True, CONF_ALL_UPDATES: True, CONF_OVERRIDE_CHOST: True}, - ) + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + { + CONF_DISABLE_RTSP: True, + CONF_ALL_UPDATES: True, + CONF_OVERRIDE_CHOST: True, + }, + ) - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["data"] == { - "all_updates": True, - "disable_rtsp": True, - "override_connection_host": True, - "max_media": 1000, - } + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "all_updates": True, + "disable_rtsp": True, + "override_connection_host": True, + "max_media": 1000, + "allow_ea": False, + } + await hass.config_entries.async_unload(mock_config.entry_id) @pytest.mark.parametrize( diff --git a/tests/components/unifiprotect/test_diagnostics.py b/tests/components/unifiprotect/test_diagnostics.py index a0ed8f0d882..396cc13efa9 100644 --- a/tests/components/unifiprotect/test_diagnostics.py +++ b/tests/components/unifiprotect/test_diagnostics.py @@ -2,6 +2,7 @@ from pyunifiprotect.data import NVR, Light +from homeassistant.components.unifiprotect.const import CONF_ALLOW_EA from homeassistant.core import HomeAssistant from .utils import MockUFPFixture, init_entry @@ -16,12 +17,23 @@ async def test_diagnostics( await init_entry(hass, ufp, [light]) + options = dict(ufp.entry.options) + options[CONF_ALLOW_EA] = True + hass.config_entries.async_update_entry(ufp.entry, options=options) + await hass.async_block_till_done() + diag = await get_diagnostics_for_config_entry(hass, hass_client, ufp.entry) + assert "options" in diag and isinstance(diag["options"], dict) + options = diag["options"] + assert options[CONF_ALLOW_EA] is True + + assert "bootstrap" in diag and isinstance(diag["bootstrap"], dict) + bootstrap = diag["bootstrap"] nvr: NVR = ufp.api.bootstrap.nvr # validate some of the data - assert "nvr" in diag and isinstance(diag["nvr"], dict) - nvr_dict = diag["nvr"] + assert "nvr" in bootstrap and isinstance(bootstrap["nvr"], dict) + nvr_dict = bootstrap["nvr"] # should have been anonymized assert nvr_dict["id"] != nvr.id assert nvr_dict["mac"] != nvr.mac @@ -32,11 +44,11 @@ async def test_diagnostics( assert nvr_dict["type"] == nvr.type assert ( - "lights" in diag - and isinstance(diag["lights"], list) - and len(diag["lights"]) == 1 + "lights" in bootstrap + and isinstance(bootstrap["lights"], list) + and len(bootstrap["lights"]) == 1 ) - light_dict = diag["lights"][0] + light_dict = bootstrap["lights"][0] # should have been anonymized assert light_dict["id"] != light.id assert light_dict["name"] != light.mac diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 9392caa30ac..04b4928aaec 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -72,7 +72,9 @@ async def test_setup_multiple( nvr.id ufp.api.get_nvr = AsyncMock(return_value=nvr) - with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + with patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api: mock_config = MockConfigEntry( domain=DOMAIN, data={ @@ -194,7 +196,7 @@ async def test_setup_starts_discovery( ): """Test setting up will start discovery.""" with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" + "homeassistant.components.unifiprotect.utils.ProtectApiClient" ) as mock_api: ufp_config_entry.add_to_hass(hass) mock_api.return_value = ufp_client diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 8200a1323ab..3447cdfc139 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -222,7 +222,9 @@ async def test_browse_media_root_multiple_consoles( api2.update = AsyncMock(return_value=bootstrap2) api2.async_disconnect_ws = AsyncMock() - with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + with patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api: mock_config = MockConfigEntry( domain=DOMAIN, data={ @@ -285,7 +287,9 @@ async def test_browse_media_root_multiple_consoles_only_one_media( api2.update = AsyncMock(return_value=bootstrap2) api2.async_disconnect_ws = AsyncMock() - with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + with patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api: mock_config = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py new file mode 100644 index 00000000000..3ffd2ea4a43 --- /dev/null +++ b/tests/components/unifiprotect/test_repairs.py @@ -0,0 +1,126 @@ +"""Test repairs for unifiprotect.""" + +from __future__ import annotations + +from copy import copy +from http import HTTPStatus +from unittest.mock import Mock + +from pyunifiprotect.data import Version + +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.components.unifiprotect.const import DOMAIN +from homeassistant.core import HomeAssistant + +from .utils import MockUFPFixture, init_entry + + +async def test_ea_warning_ignore( + hass: HomeAssistant, + ufp: MockUFPFixture, + hass_client, + hass_ws_client, +): + """Test EA warning is created if using prerelease version of Protect.""" + + version = ufp.api.bootstrap.nvr.version + assert version.is_prerelease + await init_entry(hass, ufp, []) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "ea_warning": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"version": str(version)} + assert data["step_id"] == "start" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"version": str(version)} + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + + +async def test_ea_warning_fix( + hass: HomeAssistant, + ufp: MockUFPFixture, + hass_client, + hass_ws_client, +): + """Test EA warning is created if using prerelease version of Protect.""" + + version = ufp.api.bootstrap.nvr.version + assert version.is_prerelease + await init_entry(hass, ufp, []) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "ea_warning": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"version": str(version)} + assert data["step_id"] == "start" + + new_nvr = copy(ufp.api.bootstrap.nvr) + new_nvr.version = Version("2.2.6") + mock_msg = Mock() + mock_msg.changed_data = {"version": "2.2.6"} + mock_msg.new_obj = new_nvr + + ufp.api.bootstrap.nvr = new_nvr + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index a712c112b6d..f5779e78b1c 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -13,7 +13,7 @@ from pyunifiprotect.data import ( Sensor, SmartDetectObjectType, ) -from pyunifiprotect.data.nvr import EventMetadata +from pyunifiprotect.data.nvr import EventMetadata, LicensePlateMetadata from homeassistant.components.unifiprotect.const import ( ATTR_EVENT_SCORE, @@ -23,7 +23,7 @@ from homeassistant.components.unifiprotect.sensor import ( ALL_DEVICES_SENSORS, CAMERA_DISABLED_SENSORS, CAMERA_SENSORS, - MOTION_SENSORS, + EVENT_SENSORS, MOTION_TRIP_SENSORS, NVR_DISABLED_SENSORS, NVR_SENSORS, @@ -62,11 +62,11 @@ async def test_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SENSOR, 25, 13) + assert_entity_counts(hass, Platform.SENSOR, 25, 12) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SENSOR, 12, 9) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SENSOR, 25, 13) + assert_entity_counts(hass, Platform.SENSOR, 25, 12) async def test_sensor_sensor_remove( @@ -318,7 +318,7 @@ async def test_sensor_setup_camera( """Test sensor entity setup for camera devices.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SENSOR, 25, 13) + assert_entity_counts(hass, Platform.SENSOR, 25, 12) entity_registry = er.async_get(hass) @@ -399,18 +399,19 @@ async def test_sensor_setup_camera( # Detected Object unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, doorbell, MOTION_SENSORS[0] + Platform.SENSOR, doorbell, EVENT_SENSORS[0] ) entity = entity_registry.async_get(entity_id) assert entity assert entity.unique_id == unique_id + await enable_entity(hass, ufp.entry.entry_id, entity_id) + state = hass.states.get(entity_id) assert state assert state.state == OBJECT_TYPE_NONE assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - assert state.attributes[ATTR_EVENT_SCORE] == 0 async def test_sensor_setup_camera_with_last_trip_time( @@ -451,12 +452,14 @@ async def test_sensor_update_motion( """Test sensor motion entity.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SENSOR, 25, 13) + assert_entity_counts(hass, Platform.SENSOR, 25, 12) _, entity_id = ids_from_device_description( - Platform.SENSOR, doorbell, MOTION_SENSORS[0] + Platform.SENSOR, doorbell, EVENT_SENSORS[0] ) + await enable_entity(hass, ufp.entry.entry_id, entity_id) + event = Event( id="test_event_id", type=EventType.SMART_DETECT, @@ -562,3 +565,54 @@ async def test_sensor_update_alarm_with_last_trip_time( == (fixed_now - timedelta(hours=1)).replace(microsecond=0).isoformat() ) assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + +async def test_camera_update_licenseplate( + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, fixed_now: datetime +): + """Test sensor motion entity.""" + + camera.feature_flags.smart_detect_types.append(SmartDetectObjectType.LICENSE_PLATE) + camera.feature_flags.has_smart_detect = True + camera.smart_detect_settings.object_types.append( + SmartDetectObjectType.LICENSE_PLATE + ) + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.SENSOR, 24, 13) + + _, entity_id = ids_from_device_description( + Platform.SENSOR, camera, EVENT_SENSORS[1] + ) + + event_metadata = EventMetadata( + license_plate=LicensePlateMetadata(name="ABCD1234", confidence_level=95) + ) + event = Event( + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE], + smart_detect_event_ids=[], + metadata=event_metadata, + api=ufp.api, + ) + + new_camera = camera.copy() + new_camera.is_smart_detected = True + new_camera.last_smart_detect_event_id = event.id + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = new_camera + + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "ABCD1234" diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 82bf90eefd4..2ede00e60f2 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -35,6 +35,8 @@ CAMERA_SWITCHES_BASIC = [ for d in CAMERA_SWITCHES if d.name != "Detections: Face" and d.name != "Detections: Package" + and d.name != "Detections: License Plate" + and d.name != "Detections: Smoke/CO" and d.name != "SSH Enabled" ] CAMERA_SWITCHES_NO_EXTRA = [ diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index 0768252f6c9..1c774a9b921 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -111,7 +111,7 @@ async def test_thumbnail( ufp: MockUFPFixture, camera: Camera, ) -> None: - """Test invalid NVR ID in URL.""" + """Test NVR ID in URL.""" ufp.api.get_event_thumbnail = AsyncMock(return_value=b"testtest") @@ -127,6 +127,28 @@ async def test_thumbnail( ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) +async def test_thumbnail_entry_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test config entry ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=b"testtest") + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.entry.entry_id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 200 + assert response.content_type == "image/jpeg" + assert await response.content.read() == b"testtest" + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + async def test_video_bad_event( hass: HomeAssistant, ufp: MockUFPFixture, @@ -425,3 +447,47 @@ async def test_video( assert response.status == 200 ufp.api.request.assert_called_once + + +async def test_video_entity_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with no video.""" + + content = Mock() + content.__anext__ = AsyncMock(side_effect=[b"test", b"test", StopAsyncIteration()]) + content.__aiter__ = Mock(return_value=content) + + mock_response = Mock() + mock_response.content_length = 8 + mock_response.content.iter_chunked = Mock(return_value=content) + + ufp.api.request = AsyncMock(return_value=mock_response) + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + url = url.replace(camera.id, "camera.test_camera_high") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + assert await response.content.read() == b"testtest" + + assert response.status == 200 + ufp.api.request.assert_called_once diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index 4bbc7c51b9d..5757469fafe 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the UpCloud config flow.""" +from unittest.mock import patch + import requests.exceptions from requests_mock import ANY from upcloud_api import UpCloudAPIError @@ -81,6 +83,10 @@ async def test_options(hass): ) config_entry.add_to_hass(hass) + with patch("homeassistant.components.upcloud.async_setup_entry", return_value=True): + 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) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index ca978af75f2..c7196fed0c5 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -877,6 +877,35 @@ async def test_async_is_plugged_in(hass, hass_ws_client): assert usb.async_is_plugged_in(hass, matcher) +@pytest.mark.parametrize( + "matcher", + [ + {"vid": "abcd"}, + {"pid": "123a"}, + {"serial_number": "1234ABCD"}, + {"manufacturer": "Some Manufacturer"}, + {"description": "A description"}, + ], +) +async def test_async_is_plugged_in_case_enforcement(hass, matcher): + """Test `async_is_plugged_in` throws an error when incorrect cases are used.""" + + new_usb = [{"domain": "test1", "vid": "ABCD"}] + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=new_usb + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + usb.async_is_plugged_in(hass, matcher) + + async def test_web_socket_triggers_discovery_request_callbacks(hass, hass_ws_client): """Test the websocket call triggers a discovery request callback.""" mock_callback = Mock() diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index ef9fd2a0e4b..0c14f359b5f 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -39,6 +39,11 @@ def patch_metrics(metrics: dict[str, Any]): ) +def patch_metrics_set(): + """Patch the Vallox metrics set values.""" + return patch("homeassistant.components.vallox.Vallox.set_values") + + @pytest.fixture(autouse=True) def patch_profile_home(): """Patch the Vallox profile response.""" diff --git a/tests/components/vallox/test_number.py b/tests/components/vallox/test_number.py new file mode 100644 index 00000000000..3d05cafaef1 --- /dev/null +++ b/tests/components/vallox/test_number.py @@ -0,0 +1,80 @@ +"""Tests for Vallox number platform.""" +import pytest + +from homeassistant.components.number.const import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .conftest import patch_metrics, patch_metrics_set + +from tests.common import MockConfigEntry + +TEST_TEMPERATURE_ENTITIES_DATA = [ + ( + "number.vallox_supply_air_temperature_home", + "A_CYC_HOME_AIR_TEMP_TARGET", + 19.0, + ), + ( + "number.vallox_supply_air_temperature_away", + "A_CYC_AWAY_AIR_TEMP_TARGET", + 18.0, + ), + ( + "number.vallox_supply_air_temperature_boost", + "A_CYC_BOOST_AIR_TEMP_TARGET", + 17.0, + ), +] + + +@pytest.mark.parametrize("entity_id, metric_key, value", TEST_TEMPERATURE_ENTITIES_DATA) +async def test_temperature_number_entities( + entity_id: str, + metric_key: str, + value: float, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test temperature entities.""" + # Arrange + metrics = {metric_key: value} + + # Act + with patch_metrics(metrics=metrics): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get(entity_id) + assert sensor.state == str(value) + assert sensor.attributes["unit_of_measurement"] == "°C" + + +@pytest.mark.parametrize("entity_id, metric_key, value", TEST_TEMPERATURE_ENTITIES_DATA) +async def test_temperature_number_entity_set( + entity_id: str, + metric_key: str, + value: float, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test temperature set.""" + # Act + with patch_metrics(metrics={}), patch_metrics_set() as metrics_set: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: value, + }, + ) + await hass.async_block_till_done() + metrics_set.assert_called_once_with({metric_key: value}) diff --git a/tests/components/vallox/test_switch.py b/tests/components/vallox/test_switch.py new file mode 100644 index 00000000000..5a3f0ebd648 --- /dev/null +++ b/tests/components/vallox/test_switch.py @@ -0,0 +1,68 @@ +"""Tests for Vallox switch platform.""" +import pytest + +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant + +from .conftest import patch_metrics, patch_metrics_set + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "entity_id, metric_key, value, expected_state", + [ + ("switch.vallox_bypass_locked", "A_CYC_BYPASS_LOCKED", 1, "on"), + ("switch.vallox_bypass_locked", "A_CYC_BYPASS_LOCKED", 0, "off"), + ], +) +async def test_switch_entities( + entity_id: str, + metric_key: str, + value: int, + expected_state: str, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test switch entities.""" + # Arrange + metrics = {metric_key: value} + + # Act + with patch_metrics(metrics=metrics): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == expected_state + + +@pytest.mark.parametrize( + "service, metric_key, value", + [ + (SERVICE_TURN_ON, "A_CYC_BYPASS_LOCKED", 1), + (SERVICE_TURN_OFF, "A_CYC_BYPASS_LOCKED", 0), + ], +) +async def test_bypass_lock_switch_entitity_set( + service: str, + metric_key: str, + value: int, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test bypass lock switch set.""" + # Act + with patch_metrics(metrics={}), patch_metrics_set() as metrics_set: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: "switch.vallox_bypass_locked"}, + ) + await hass.async_block_till_done() + metrics_set.assert_called_once_with({metric_key: value}) diff --git a/tests/components/wake_on_lan/conftest.py b/tests/components/wake_on_lan/conftest.py new file mode 100644 index 00000000000..582698e39d5 --- /dev/null +++ b/tests/components/wake_on_lan/conftest.py @@ -0,0 +1,13 @@ +"""Test fixtures for Wake on Lan.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_send_magic_packet() -> AsyncMock: + """Mock magic packet.""" + with patch("wakeonlan.send_magic_packet") as mock_send: + yield mock_send diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index 3ec7a53a436..1cfe2fa7436 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -1,14 +1,17 @@ """Tests for Wake On LAN component.""" +from __future__ import annotations + 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.core import HomeAssistant from homeassistant.setup import async_setup_component -async def test_send_magic_packet(hass): +async def test_send_magic_packet(hass: HomeAssistant) -> None: """Test of send magic packet service call.""" with patch("homeassistant.components.wake_on_lan.wakeonlan") as mocked_wakeonlan: mac = "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index cac287a87a1..b73f5223576 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,10 +1,10 @@ """The tests for the wake on lan switch platform.""" +from __future__ import annotations + import subprocess -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -import pytest - -import homeassistant.components.switch as switch +from homeassistant.components import switch from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -12,19 +12,15 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import async_mock_service -@pytest.fixture(autouse=True) -def mock_send_magic_packet(): - """Mock magic packet.""" - with patch("wakeonlan.send_magic_packet") as mock_send: - yield mock_send - - -async def test_valid_hostname(hass): +async def test_valid_hostname( + hass: HomeAssistant, mock_send_magic_packet: AsyncMock +) -> None: """Test with valid hostname.""" assert await async_setup_component( hass, @@ -65,7 +61,9 @@ async def test_valid_hostname(hass): assert state.state == STATE_ON -async def test_broadcast_config_ip_and_port(hass, mock_send_magic_packet): +async def test_broadcast_config_ip_and_port( + hass: HomeAssistant, mock_send_magic_packet: AsyncMock +) -> None: """Test with broadcast address and broadcast port config.""" mac = "00-01-02-03-04-05" broadcast_address = "255.255.255.255" @@ -102,7 +100,9 @@ async def test_broadcast_config_ip_and_port(hass, mock_send_magic_packet): ) -async def test_broadcast_config_ip(hass, mock_send_magic_packet): +async def test_broadcast_config_ip( + hass: HomeAssistant, mock_send_magic_packet: AsyncMock +) -> None: """Test with only broadcast address.""" mac = "00-01-02-03-04-05" @@ -136,7 +136,9 @@ async def test_broadcast_config_ip(hass, mock_send_magic_packet): mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address) -async def test_broadcast_config_port(hass, mock_send_magic_packet): +async def test_broadcast_config_port( + hass: HomeAssistant, mock_send_magic_packet: AsyncMock +) -> None: """Test with only broadcast port config.""" mac = "00-01-02-03-04-05" @@ -164,7 +166,9 @@ async def test_broadcast_config_port(hass, mock_send_magic_packet): mock_send_magic_packet.assert_called_with(mac, port=port) -async def test_off_script(hass): +async def test_off_script( + hass: HomeAssistant, mock_send_magic_packet: AsyncMock +) -> None: """Test with turn off script.""" assert await async_setup_component( @@ -212,7 +216,9 @@ async def test_off_script(hass): assert len(calls) == 1 -async def test_no_hostname_state(hass): +async def test_no_hostname_state( + hass: HomeAssistant, mock_send_magic_packet: AsyncMock +) -> None: """Test that the state updates if we do not pass in a hostname.""" assert await async_setup_component( diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 53af3b6383d..ed20e01cb2c 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -10,8 +10,10 @@ from homeassistant.components.wallbox.const import ( CHARGER_ADDED_RANGE_KEY, CHARGER_CHARGING_POWER_KEY, CHARGER_CHARGING_SPEED_KEY, + CHARGER_CURRENCY_KEY, CHARGER_CURRENT_VERSION_KEY, CHARGER_DATA_KEY, + CHARGER_ENERGY_PRICE_KEY, CHARGER_LOCKED_UNLOCKED_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, @@ -42,10 +44,12 @@ test_response = json.loads( CHARGER_NAME_KEY: "WallboxName", CHARGER_DATA_KEY: { CHARGER_MAX_CHARGING_CURRENT_KEY: 24, + CHARGER_ENERGY_PRICE_KEY: 0.4, CHARGER_LOCKED_UNLOCKED_KEY: False, CHARGER_SERIAL_NUMBER_KEY: "20000", CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E", CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"}, + CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, }, } ) diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index db15ce3a592..befa62340a5 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -98,41 +98,6 @@ async def test_if_fires_on_turn_on_request(hass, calls, client): assert calls[1].data["id"] == 0 -async def test_get_triggers_for_invalid_device_id(hass, caplog): - """Test error raised for invalid shelly device_id.""" - await async_setup_component(hass, "persistent_notification", {}) - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "invalid_device_id", - "type": "webostv.turn_on", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "{{ trigger.invalid_device }}", - "id": "{{ trigger.id }}", - }, - }, - } - ] - }, - ) - await hass.async_block_till_done() - - assert ( - "Invalid config for [automation]: Device invalid_device_id is not a valid webostv device" - in caplog.text - ) - - async def test_failure_scenarios(hass, client): """Test failure scenarios.""" await setup_webostv(hass) @@ -173,7 +138,3 @@ async def test_failure_scenarios(hass, client): # Test that device id from non webostv domain raises exception with pytest.raises(InvalidDeviceAutomationConfig): await device_trigger.async_validate_trigger_config(hass, config) - - # Test no exception if device is not loaded - await hass.config_entries.async_unload(entry.entry_id) - assert await device_trigger.async_validate_trigger_config(hass, config) == config diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 721a4da9383..6e188b68219 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -3,9 +3,13 @@ import asyncio from unittest.mock import patch import aiohttp +from aiohttp.client_exceptions import ClientConnectionError from homeassistant import config_entries from homeassistant.components.whirlpool.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -56,8 +60,8 @@ async def test_form_invalid_auth(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) assert result2["type"] == "form" @@ -76,8 +80,8 @@ async def test_form_cannot_connect(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) assert result2["type"] == "form" @@ -96,8 +100,8 @@ async def test_form_auth_timeout(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) assert result2["type"] == "form" @@ -116,8 +120,8 @@ async def test_form_generic_auth_exception(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) assert result2["type"] == "form" @@ -128,7 +132,7 @@ async def test_form_already_configured(hass): """Test we handle cannot connect error.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -147,11 +151,140 @@ async def test_form_already_configured(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "already_configured" + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.whirlpool.async_setup_entry", + return_value=True, + ), patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + } + + +async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: + """Test an authorization error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + with patch( + "homeassistant.components.whirlpool.async_setup_entry", + return_value=True, + ), patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_flow_connnection_error(hass: HomeAssistant) -> None: + """Test a connection error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.whirlpool.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.whirlpool.config_flow.Auth.do_auth", + side_effect=ClientConnectionError, + ), patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 83e8a622e78..70c4d177e59 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -177,6 +177,10 @@ async def test_set_config_unique_id( spec=DataUpdateCoordinator ) data_manager.poll_data_update_coordinator.last_update_success = True + data_manager.subscription_update_coordinator = MagicMock( + spec=DataUpdateCoordinator + ) + data_manager.subscription_update_coordinator.last_update_success = True mock.return_value = data_manager config_entry.add_to_hass(hass) diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index f8ab8794c0d..f0d2d6b0681 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -125,6 +125,10 @@ class TestWorkdaySetup: with pytest.raises(vol.Invalid): binary_sensor.valid_country("HomeAssistantLand") + # Valid country code validation must not raise an exception + for country in ("IM", "LI", "US"): + assert binary_sensor.valid_country(country) == country + def test_setup_component_province(self): """Set up workday component.""" with assert_setup_component(1, "binary_sensor"): diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index ab88cc559b7..a6d66ecf6df 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -84,6 +84,20 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=False, ) +HHCCJCY10_SERVICE_INFO = BluetoothServiceInfoBleak( + name="HHCCJCY10", + address="DC:23:4D:E5:5B:FC", + device=BLEDevice("00:00:00:00:00:00", None), + rssi=-56, + manufacturer_data={}, + service_data={"0000fd50-0000-1000-8000-00805f9b34fb": b"\x0e\x00n\x014\xa4(\x00["}, + service_uuids=["0000fd50-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=generate_advertisement_data(local_name="Not it"), + time=0, + connectable=False, +) + MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( name="LYWSD02MMC", address="A4:C1:38:56:53:84", diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py index 2997943468f..3d68d78e27e 100644 --- a/tests/components/xiaomi_ble/conftest.py +++ b/tests/components/xiaomi_ble/conftest.py @@ -20,7 +20,6 @@ class MockBleakClient: def __init__(self, *args, **kwargs): """Mock BleakClient.""" - pass async def __aenter__(self, *args, **kwargs): """Mock BleakClient.__aenter__.""" @@ -28,15 +27,12 @@ class MockBleakClient: async def __aexit__(self, *args, **kwargs): """Mock BleakClient.__aexit__.""" - pass async def connect(self, *args, **kwargs): """Mock BleakClient.connect.""" - pass async def disconnect(self, *args, **kwargs): """Mock BleakClient.disconnect.""" - pass class MockBleakClientBattery5(MockBleakClient): diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 17f9254b5ff..25f15938c6c 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT -from . import MMC_T201_1_SERVICE_INFO, make_advertisement +from . import HHCCJCY10_SERVICE_INFO, MMC_T201_1_SERVICE_INFO, make_advertisement from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info_bleak @@ -433,3 +433,58 @@ async def test_xiaomi_CGDK2(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_hhcc_HHCCJCY10(hass): + """This device used a different UUID compared to the other Xiaomi sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="DC:23:4D:E5:5B:FC", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + inject_bluetooth_service_info_bleak(hass, HHCCJCY10_SERVICE_INFO) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 5 + + temp_sensor = hass.states.get("sensor.plant_sensor_5bfc_temperature") + temp_sensor_attr = temp_sensor.attributes + assert temp_sensor.state == "11.0" + assert temp_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Temperature" + assert temp_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + illu_sensor = hass.states.get("sensor.plant_sensor_5bfc_illuminance") + illu_sensor_attr = illu_sensor.attributes + assert illu_sensor.state == "79012" + assert illu_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Illuminance" + assert illu_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illu_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + cond_sensor = hass.states.get("sensor.plant_sensor_5bfc_conductivity") + cond_sensor_attr = cond_sensor.attributes + assert cond_sensor.state == "91" + assert cond_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Conductivity" + assert cond_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + moist_sensor = hass.states.get("sensor.plant_sensor_5bfc_moisture") + moist_sensor_attr = moist_sensor.attributes + assert moist_sensor.state == "14" + assert moist_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Moisture" + assert moist_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert moist_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + bat_sensor = hass.states.get("sensor.plant_sensor_5bfc_battery") + bat_sensor_attr = bat_sensor.attributes + assert bat_sensor.state == "40" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Battery" + assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index a4d29224b74..e002d8ce03b 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -57,6 +57,7 @@ def zigpy_app_controller(): type(app).nwk = PropertyMock(return_value=zigpy.types.NWK(0x0000)) type(app).devices = PropertyMock(return_value={}) type(app).backups = zigpy.backups.BackupManager(app) + type(app).topology = zigpy.topology.Topology(app) state = State() state.node_info.ieee = app.ieee.return_value diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index f1b900400ea..d00e6bca795 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -486,7 +486,7 @@ async def test_climate_hvac_action_pi_demand(hass, device_climate): ), ) async def test_hvac_mode(hass, device_climate, sys_mode, hvac_mode): - """Test HVAC modee.""" + """Test HVAC mode.""" thrm_cluster = device_climate.device.endpoints[1].thermostat entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 725f9cc0917..d457e0b6b8c 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -16,7 +16,7 @@ import zigpy.types from homeassistant import config_entries from homeassistant.components import ssdp, usb, zeroconf from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL -from homeassistant.components.zha import config_flow +from homeassistant.components.zha import config_flow, radio_manager from homeassistant.components.zha.core.const import ( CONF_BAUDRATE, CONF_FLOWCONTROL, @@ -49,7 +49,7 @@ def disable_platform_only(): @pytest.fixture(autouse=True) def reduce_reconnect_timeout(): """Reduces reconnect timeout to speed up tests.""" - with patch("homeassistant.components.zha.config_flow.CONNECT_DELAY_S", 0.01): + with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.01): yield @@ -76,12 +76,12 @@ def backup(): def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): - """Mock `_detect_radio_type` that just sets the appropriate attributes.""" + """Mock `detect_radio_type` that just sets the appropriate attributes.""" async def detect(self): - self._radio_type = radio_type - self._device_settings = radio_type.controller.SCHEMA_DEVICE( - {CONF_DEVICE_PATH: self._device_path} + self.radio_type = radio_type + self.device_settings = radio_type.controller.SCHEMA_DEVICE( + {CONF_DEVICE_PATH: self.device_path} ) return ret @@ -669,7 +669,7 @@ async def test_discovery_already_setup(hass): @patch( - "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._detect_radio_type", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(radio_type=RadioType.deconz), ) @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @@ -707,7 +707,7 @@ async def test_user_flow(hass): @patch( - "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._detect_radio_type", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(ret=False), ) @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @@ -799,12 +799,14 @@ async def test_detect_radio_type_success( """Test detect radios successfully.""" handler = config_flow.ZhaConfigFlowHandler() - handler._device_path = "/dev/null" + handler._radio_mgr.device_path = "/dev/null" - await handler._detect_radio_type() + await handler._radio_mgr.detect_radio_type() - assert handler._radio_type == RadioType.znp - assert handler._device_settings[zigpy.config.CONF_DEVICE_PATH] == "/dev/null" + assert handler._radio_mgr.radio_type == RadioType.znp + assert ( + handler._radio_mgr.device_settings[zigpy.config.CONF_DEVICE_PATH] == "/dev/null" + ) assert bellows_probe.await_count == 1 assert znp_probe.await_count == 1 @@ -825,12 +827,14 @@ async def test_detect_radio_type_success_with_settings( """Test detect radios successfully but probing returns new settings.""" handler = config_flow.ZhaConfigFlowHandler() - handler._device_path = "/dev/null" - await handler._detect_radio_type() + handler._radio_mgr.device_path = "/dev/null" + await handler._radio_mgr.detect_radio_type() - assert handler._radio_type == RadioType.ezsp - assert handler._device_settings["new_setting"] == 123 - assert handler._device_settings[zigpy.config.CONF_DEVICE_PATH] == "/dev/null" + assert handler._radio_mgr.radio_type == RadioType.ezsp + assert handler._radio_mgr.device_settings["new_setting"] == 123 + assert ( + handler._radio_mgr.device_settings[zigpy.config.CONF_DEVICE_PATH] == "/dev/null" + ) assert bellows_probe.await_count == 1 assert znp_probe.await_count == 0 @@ -987,6 +991,7 @@ async def test_hardware_already_setup(hass): ).add_to_hass(hass) data = { + "name": "Yellow", "radio_type": "efr32", "port": { "path": "/dev/ttyAMA1", @@ -1019,7 +1024,7 @@ async def test_hardware_invalid_data(hass, data): def test_allow_overwrite_ezsp_ieee(): """Test modifying the backup to allow bellows to override the IEEE address.""" backup = zigpy.backups.NetworkBackup() - new_backup = config_flow._allow_overwrite_ezsp_ieee(backup) + new_backup = radio_manager._allow_overwrite_ezsp_ieee(backup) assert backup != new_backup assert new_backup.network_info.stack_specific["ezsp"][EZSP_OVERWRITE_EUI64] is True @@ -1029,7 +1034,7 @@ def test_prevent_overwrite_ezsp_ieee(): """Test modifying the backup to prevent bellows from overriding the IEEE address.""" backup = zigpy.backups.NetworkBackup() backup.network_info.stack_specific["ezsp"] = {EZSP_OVERWRITE_EUI64: True} - new_backup = config_flow._prevent_overwrite_ezsp_ieee(backup) + new_backup = radio_manager._prevent_overwrite_ezsp_ieee(backup) assert backup != new_backup assert not new_backup.network_info.stack_specific.get("ezsp", {}).get( @@ -1046,7 +1051,7 @@ def pick_radio(hass): port_select = f"{port}, s/n: {port.serial_number} - {port.manufacturer}" with patch( - "homeassistant.components.zha.config_flow.ZhaConfigFlowHandler._detect_radio_type", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(radio_type=radio_type), ): result = await hass.config_entries.flow.async_init( @@ -1126,7 +1131,7 @@ def test_parse_uploaded_backup(process_mock): assert backup == parsed_backup -@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +@patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_formation_strategy_restore_manual_backup_non_ezsp( allow_overwrite_ieee_mock, pick_radio, mock_app, hass ): @@ -1158,7 +1163,7 @@ async def test_formation_strategy_restore_manual_backup_non_ezsp( assert result3["data"][CONF_RADIO_TYPE] == "znp" -@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +@patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( allow_overwrite_ieee_mock, pick_radio, mock_app, backup, hass ): @@ -1198,7 +1203,7 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( assert result4["data"][CONF_RADIO_TYPE] == "ezsp" -@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +@patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_formation_strategy_restore_manual_backup_ezsp( allow_overwrite_ieee_mock, pick_radio, mock_app, hass ): @@ -1386,7 +1391,7 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( assert result3["data"][CONF_RADIO_TYPE] == "znp" -@patch("homeassistant.components.zha.config_flow._allow_overwrite_ezsp_ieee") +@patch("homeassistant.components.zha.radio_manager._allow_overwrite_ezsp_ieee") async def test_ezsp_restore_without_settings_change_ieee( allow_overwrite_ieee_mock, pick_radio, mock_app, backup, hass ): @@ -1630,6 +1635,7 @@ async def test_options_flow_defaults_socket(hass): assert result5["step_id"] == "choose_formation_strategy" +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @patch("homeassistant.components.zha.async_setup_entry", return_value=True) async def test_options_flow_restarts_running_zha_if_cancelled(async_setup_entry, hass): """Test options flow restarts a previously-running ZHA if it's cancelled.""" @@ -1682,6 +1688,7 @@ async def test_options_flow_restarts_running_zha_if_cancelled(async_setup_entry, async_setup_entry.assert_called_once_with(hass, entry) +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_options_flow_migration_reset_old_adapter(hass, mock_app): """Test options flow for migrating from an old radio.""" diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py new file mode 100644 index 00000000000..671f831fcce --- /dev/null +++ b/tests/components/zha/test_radio_manager.py @@ -0,0 +1,372 @@ +"""Tests for ZHA config flow.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, create_autospec, patch + +import pytest +import serial.tools.list_ports +from zigpy.backups import BackupManager +import zigpy.config +from zigpy.config import CONF_DEVICE_PATH +import zigpy.types + +from homeassistant import config_entries +from homeassistant.components.usb import UsbServiceInfo +from homeassistant.components.zha import radio_manager +from homeassistant.components.zha.core.const import DOMAIN, RadioType +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +PROBE_FUNCTION_PATH = "zigbee.application.ControllerApplication.probe" + + +@pytest.fixture(autouse=True) +def disable_platform_only(): + """Disable platforms to speed up tests.""" + with patch("homeassistant.components.zha.PLATFORMS", []): + yield + + +@pytest.fixture(autouse=True) +def reduce_reconnect_timeout(): + """Reduces reconnect timeout to speed up tests.""" + with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.0001): + yield + + +@pytest.fixture(autouse=True) +def mock_app(): + """Mock zigpy app interface.""" + mock_app = AsyncMock() + mock_app.backups = create_autospec(BackupManager, instance=True) + mock_app.backups.backups = [] + + with patch( + "zigpy.application.ControllerApplication.new", AsyncMock(return_value=mock_app) + ): + yield mock_app + + +@pytest.fixture +def backup(): + """Zigpy network backup with non-default settings.""" + backup = zigpy.backups.NetworkBackup() + backup.node_info.ieee = zigpy.types.EUI64.convert("AA:BB:CC:DD:11:22:33:44") + + return backup + + +def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): + """Mock `detect_radio_type` that just sets the appropriate attributes.""" + + async def detect(self): + self.radio_type = radio_type + self.device_settings = radio_type.controller.SCHEMA_DEVICE( + {CONF_DEVICE_PATH: self.device_path} + ) + + return ret + + return detect + + +def com_port(device="/dev/ttyUSB1234"): + """Mock of a serial port.""" + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = device + port.description = "Some serial port" + + return port + + +@pytest.fixture() +def mock_connect_zigpy_app() -> Generator[None, None, None]: + """Mock the radio connection.""" + + mock_connect_app = MagicMock() + mock_connect_app.__aenter__.return_value.backups.backups = [MagicMock()] + mock_connect_app.__aenter__.return_value.backups.create_backup.return_value = ( + MagicMock() + ) + + with patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + return_value=mock_connect_app, + ): + yield + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migrate_matching_port( + hass: HomeAssistant, + mock_connect_zigpy_app, +) -> None: + """Test automatic migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=DOMAIN, + options={}, + title="Test", + version=3, + ) + config_entry.add_to_hass(hass) + + migration_data = { + "new_discovery_info": { + "name": "Test Updated", + "port": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + }, + "old_discovery_info": { + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } + }, + } + + migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) + assert await migration_helper.async_initiate_migration(migration_data) + + # Check the ZHA config entry data is updated + assert config_entry.data == { + "device": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + } + assert config_entry.title == "Test Updated" + + await migration_helper.async_finish_migration() + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + mock_detect_radio_type(), +) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migrate_matching_port_usb( + hass: HomeAssistant, + mock_connect_zigpy_app, +) -> None: + """Test automatic migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=DOMAIN, + options={}, + title="Test", + version=3, + ) + config_entry.add_to_hass(hass) + + migration_data = { + "new_discovery_info": { + "name": "Test Updated", + "port": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + }, + "old_discovery_info": { + "usb": UsbServiceInfo("/dev/ttyTEST123", "blah", "blah", None, None, None) + }, + } + + migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) + assert await migration_helper.async_initiate_migration(migration_data) + + # Check the ZHA config entry data is updated + assert config_entry.data == { + "device": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + } + assert config_entry.title == "Test Updated" + + await migration_helper.async_finish_migration() + + +async def test_migrate_matching_port_config_entry_not_loaded( + hass: HomeAssistant, + mock_connect_zigpy_app, +) -> None: + """Test automatic migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=DOMAIN, + options={}, + title="Test", + ) + config_entry.add_to_hass(hass) + config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + migration_data = { + "new_discovery_info": { + "name": "Test Updated", + "port": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + }, + "old_discovery_info": { + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } + }, + } + + migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) + assert await migration_helper.async_initiate_migration(migration_data) + + # Check the ZHA config entry data is updated + assert config_entry.data == { + "device": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + } + assert config_entry.title == "Test Updated" + + await migration_helper.async_finish_migration() + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.async_restore_backup_step_1", + side_effect=OSError, +) +async def test_migrate_matching_port_retry( + mock_restore_backup_step_1, + hass: HomeAssistant, + mock_connect_zigpy_app, +) -> None: + """Test automatic migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=DOMAIN, + options={}, + title="Test", + ) + config_entry.add_to_hass(hass) + config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + migration_data = { + "new_discovery_info": { + "name": "Test Updated", + "port": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + }, + "old_discovery_info": { + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } + }, + } + + migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) + assert await migration_helper.async_initiate_migration(migration_data) + + # Check the ZHA config entry data is updated + assert config_entry.data == { + "device": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + } + assert config_entry.title == "Test Updated" + + with pytest.raises(OSError): + await migration_helper.async_finish_migration() + assert mock_restore_backup_step_1.call_count == 100 + + +async def test_migrate_non_matching_port( + hass: HomeAssistant, + mock_connect_zigpy_app, +) -> None: + """Test automatic migration.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=DOMAIN, + options={}, + title="Test", + ) + config_entry.add_to_hass(hass) + + migration_data = { + "new_discovery_info": { + "name": "Test Updated", + "port": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + }, + "old_discovery_info": { + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST456", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } + }, + } + + migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) + assert not await migration_helper.async_initiate_migration(migration_data) + + # Check the ZHA config entry data is not updated + assert config_entry.data == { + "device": {"path": "/dev/ttyTEST123"}, + "radio_type": "ezsp", + } + assert config_entry.title == "Test" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 68052aeaab1..ba97cfe4c36 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -26,10 +26,11 @@ def addon_info_side_effect_fixture(): def mock_addon_info(addon_info_side_effect): """Mock Supervisor add-on info.""" with patch( - "homeassistant.components.zwave_js.addon.async_get_addon_info", + "homeassistant.components.hassio.addon_manager.async_get_addon_info", side_effect=addon_info_side_effect, ) as addon_info: addon_info.return_value = { + "hostname": None, "options": {}, "state": None, "update_available": False, @@ -48,7 +49,7 @@ def addon_store_info_side_effect_fixture(): def mock_addon_store_info(addon_store_info_side_effect): """Mock Supervisor add-on info.""" with patch( - "homeassistant.components.zwave_js.addon.async_get_addon_store_info", + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info", side_effect=addon_store_info_side_effect, ) as addon_store_info: addon_store_info.return_value = { @@ -112,7 +113,7 @@ def set_addon_options_side_effect_fixture(addon_options): def mock_set_addon_options(set_addon_options_side_effect): """Mock set add-on options.""" with patch( - "homeassistant.components.zwave_js.addon.async_set_addon_options", + "homeassistant.components.hassio.addon_manager.async_set_addon_options", side_effect=set_addon_options_side_effect, ) as set_options: yield set_options @@ -139,7 +140,7 @@ def install_addon_side_effect_fixture(addon_store_info, addon_info): def mock_install_addon(install_addon_side_effect): """Mock install add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_install_addon", + "homeassistant.components.hassio.addon_manager.async_install_addon", side_effect=install_addon_side_effect, ) as install_addon: yield install_addon @@ -149,7 +150,7 @@ def mock_install_addon(install_addon_side_effect): def mock_update_addon(): """Mock update add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_update_addon" + "homeassistant.components.hassio.addon_manager.async_update_addon" ) as update_addon: yield update_addon @@ -174,7 +175,7 @@ def start_addon_side_effect_fixture(addon_store_info, addon_info): def mock_start_addon(start_addon_side_effect): """Mock start add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_start_addon", + "homeassistant.components.hassio.addon_manager.async_start_addon", side_effect=start_addon_side_effect, ) as start_addon: yield start_addon @@ -184,7 +185,7 @@ def mock_start_addon(start_addon_side_effect): def stop_addon_fixture(): """Mock stop add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_stop_addon" + "homeassistant.components.hassio.addon_manager.async_stop_addon" ) as stop_addon: yield stop_addon @@ -199,7 +200,7 @@ def restart_addon_side_effect_fixture(): def mock_restart_addon(restart_addon_side_effect): """Mock restart add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_restart_addon", + "homeassistant.components.hassio.addon_manager.async_restart_addon", side_effect=restart_addon_side_effect, ) as restart_addon: yield restart_addon @@ -209,7 +210,7 @@ def mock_restart_addon(restart_addon_side_effect): def uninstall_addon_fixture(): """Mock uninstall add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_uninstall_addon" + "homeassistant.components.hassio.addon_manager.async_uninstall_addon" ) as uninstall_addon: yield uninstall_addon @@ -218,7 +219,7 @@ def uninstall_addon_fixture(): def create_backup_fixture(): """Mock create backup.""" with patch( - "homeassistant.components.zwave_js.addon.async_create_backup" + "homeassistant.components.hassio.addon_manager.async_create_backup" ) as create_backup: yield create_backup @@ -582,7 +583,9 @@ def lock_home_connect_620_state_fixture(): @pytest.fixture(name="client") -def mock_client_fixture(controller_state, version_state, log_config_state): +def mock_client_fixture( + controller_state, controller_node_state, version_state, log_config_state +): """Mock a client.""" with patch( @@ -607,6 +610,8 @@ def mock_client_fixture(controller_state, version_state, log_config_state): client.listen = AsyncMock(side_effect=listen) client.disconnect = AsyncMock(side_effect=disconnect) client.driver = Driver(client, controller_state, log_config_state) + node = Node(client, copy.deepcopy(controller_node_state)) + client.driver.controller.nodes[node.node_id] = node client.version = VersionInfo.from_message(version_state) client.ws_server_url = "ws://test:3000/zjs" @@ -614,14 +619,6 @@ def mock_client_fixture(controller_state, version_state, log_config_state): yield client -@pytest.fixture(name="controller_node") -def controller_node_fixture(client, controller_node_state): - """Mock a controller node.""" - node = Node(client, copy.deepcopy(controller_node_state)) - client.driver.controller.nodes[node.node_id] = node - return node - - @pytest.fixture(name="multisensor_6") def multisensor_6_fixture(client, multisensor_6_state): """Mock a multisensor 6 node.""" diff --git a/tests/components/zwave_js/test_addon.py b/tests/components/zwave_js/test_addon.py deleted file mode 100644 index 45f732c1aa2..00000000000 --- a/tests/components/zwave_js/test_addon.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tests for Z-Wave JS addon module.""" -import pytest - -from homeassistant.components.zwave_js.addon import AddonError, get_addon_manager -from homeassistant.components.zwave_js.const import ( - CONF_ADDON_DEVICE, - CONF_ADDON_S0_LEGACY_KEY, - CONF_ADDON_S2_ACCESS_CONTROL_KEY, - CONF_ADDON_S2_AUTHENTICATED_KEY, - CONF_ADDON_S2_UNAUTHENTICATED_KEY, -) - - -async def test_not_installed_raises_exception(hass, addon_not_installed): - """Test addon not installed raises exception.""" - addon_manager = get_addon_manager(hass) - - addon_config = { - CONF_ADDON_DEVICE: "/test", - CONF_ADDON_S0_LEGACY_KEY: "123", - CONF_ADDON_S2_ACCESS_CONTROL_KEY: "456", - CONF_ADDON_S2_AUTHENTICATED_KEY: "789", - CONF_ADDON_S2_UNAUTHENTICATED_KEY: "012", - } - - with pytest.raises(AddonError): - await addon_manager.async_configure_addon(addon_config) - - with pytest.raises(AddonError): - await addon_manager.async_update_addon() diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index 336d3688988..27bd41b3142 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -10,7 +10,6 @@ async def test_ping_entity( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, - controller_node, integration, caplog, ): diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index f58b4187469..eacf4b61cc8 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -88,7 +88,7 @@ def discovery_info_side_effect_fixture(): def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect): """Mock get add-on discovery info.""" with patch( - "homeassistant.components.zwave_js.addon.async_get_addon_discovery_info", + "homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info", side_effect=discovery_info_side_effect, return_value=discovery_info, ) as get_addon_discovery_info: diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index ad9d61b3f33..a60f10761b6 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -91,6 +91,16 @@ async def test_get_actions( for action in expected_actions: assert action in actions + # Test that we don't return actions for a controller node + device = dev_reg.async_get_device( + {get_device_id(driver, client.driver.controller.nodes[1])} + ) + assert device + assert ( + await async_get_device_automations(hass, DeviceAutomationType.ACTION, device.id) + == [] + ) + async def test_get_actions_meter( hass: HomeAssistant, @@ -408,9 +418,10 @@ async def test_get_action_capabilities( ): """Test we get the expected action capabilities.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, climate_radio_thermostat_ct100_plus)} + ) + assert device # Test refresh_value capabilities = await device_action.async_get_action_capabilities( diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 8b538a3c48b..b42573f3aa9 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -15,7 +15,10 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.components.zwave_js import DOMAIN, device_condition -from homeassistant.components.zwave_js.helpers import get_zwave_value_from_config +from homeassistant.components.zwave_js.helpers import ( + get_device_id, + get_zwave_value_from_config, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component @@ -32,9 +35,10 @@ def calls(hass): async def test_get_conditions(hass, client, lock_schlage_be469, integration) -> None: """Test we get the expected onditions from a zwave_js.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device config_value = list(lock_schlage_be469.get_configuration_values().values())[0] value_id = config_value.value_id name = config_value.property_name @@ -70,15 +74,28 @@ async def test_get_conditions(hass, client, lock_schlage_be469, integration) -> for condition in expected_conditions: assert condition in conditions + # Test that we don't return actions for a controller node + device = dev_reg.async_get_device( + {get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device + assert ( + await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device.id + ) + == [] + ) + async def test_node_status_state( hass, client, lock_schlage_be469, integration, calls ) -> None: """Test for node_status conditions.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device assert await async_setup_component( hass, @@ -224,9 +241,10 @@ async def test_config_parameter_state( ) -> None: """Test for config_parameter conditions.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device assert await async_setup_component( hass, @@ -333,9 +351,10 @@ async def test_value_state( ) -> None: """Test for value conditions.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device assert await async_setup_component( hass, @@ -377,9 +396,10 @@ async def test_get_condition_capabilities_node_status( ): """Test we don't get capabilities from a node_status condition.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device capabilities = await device_condition.async_get_condition_capabilities( hass, @@ -413,9 +433,10 @@ async def test_get_condition_capabilities_value( ): """Test we get the expected capabilities from a value condition.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device capabilities = await device_condition.async_get_condition_capabilities( hass, @@ -462,9 +483,10 @@ async def test_get_condition_capabilities_config_parameter( """Test we get the expected capabilities from a config_parameter condition.""" node = climate_radio_thermostat_ct100_plus dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, climate_radio_thermostat_ct100_plus)} + ) + assert device # Test enumerated type param capabilities = await device_condition.async_get_condition_capabilities( @@ -541,9 +563,10 @@ async def test_get_condition_capabilities_config_parameter( async def test_failure_scenarios(hass, client, hank_binary_switch, integration): """Test failure scenarios.""" dev_reg = device_registry.async_get(hass) - device = device_registry.async_entries_for_config_entry( - dev_reg, integration.entry_id - )[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, hank_binary_switch)} + ) + assert device with pytest.raises(HomeAssistantError): await device_condition.async_condition_from_config( diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 859164aa4c3..e9bc319fe4d 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -15,13 +15,11 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.zwave_js import DOMAIN, device_trigger from homeassistant.components.zwave_js.helpers import ( async_get_node_status_sensor_entity_id, + get_device_id, ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import ( - async_entries_for_config_entry, - async_get as async_get_dev_reg, -) +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from homeassistant.setup import async_setup_component @@ -38,12 +36,30 @@ def calls(hass): return async_mock_service(hass, "test", "automation") +async def test_no_controller_triggers(hass, client, integration): + """Test that we do not get triggers for the controller.""" + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device( + {get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device + assert ( + await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + == [] + ) + + async def test_get_notification_notification_triggers( hass, client, lock_schlage_be469, integration ): """Test we get the expected triggers from a zwave_js device with the Notification CC.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device expected_trigger = { "platform": "device", "domain": DOMAIN, @@ -64,7 +80,10 @@ async def test_if_notification_notification_fires( """Test for event.notification.notification trigger firing.""" node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device assert await async_setup_component( hass, @@ -157,7 +176,10 @@ async def test_get_trigger_capabilities_notification_notification( ): """Test we get the expected capabilities from a notification.notification trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -189,7 +211,10 @@ async def test_if_entry_control_notification_fires( """Test for notification.entry_control trigger firing.""" node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device assert await async_setup_component( hass, @@ -281,7 +306,10 @@ async def test_get_trigger_capabilities_entry_control_notification( ): """Test we get the expected capabilities from a notification.entry_control trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -308,7 +336,10 @@ async def test_get_trigger_capabilities_entry_control_notification( async def test_get_node_status_triggers(hass, client, lock_schlage_be469, integration): """Test we get the expected triggers from a device with node status sensor enabled.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device ent_reg = async_get_ent_reg(hass) entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg @@ -337,7 +368,10 @@ async def test_if_node_status_change_fires( """Test for node_status trigger firing.""" node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device ent_reg = async_get_ent_reg(hass) entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg @@ -412,7 +446,10 @@ async def test_get_trigger_capabilities_node_status( ): """Test we get the expected capabilities from a node_status trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device ent_reg = async_get_ent_reg(hass) entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg @@ -467,7 +504,10 @@ async def test_get_basic_value_notification_triggers( ): """Test we get the expected triggers from a zwave_js device with the Basic CC.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, ge_in_wall_dimmer_switch)} + ) + assert device expected_trigger = { "platform": "device", "domain": DOMAIN, @@ -492,7 +532,10 @@ async def test_if_basic_value_notification_fires( """Test for event.value_notification.basic trigger firing.""" node: Node = ge_in_wall_dimmer_switch dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, ge_in_wall_dimmer_switch)} + ) + assert device assert await async_setup_component( hass, @@ -600,7 +643,10 @@ async def test_get_trigger_capabilities_basic_value_notification( ): """Test we get the expected capabilities from a value_notification.basic trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, ge_in_wall_dimmer_switch)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -635,7 +681,10 @@ async def test_get_central_scene_value_notification_triggers( ): """Test we get the expected triggers from a zwave_js device with the Central Scene CC.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, wallmote_central_scene)} + ) + assert device expected_trigger = { "platform": "device", "domain": DOMAIN, @@ -660,7 +709,10 @@ async def test_if_central_scene_value_notification_fires( """Test for event.value_notification.central_scene trigger firing.""" node: Node = wallmote_central_scene dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, wallmote_central_scene)} + ) + assert device assert await async_setup_component( hass, @@ -775,7 +827,10 @@ async def test_get_trigger_capabilities_central_scene_value_notification( ): """Test we get the expected capabilities from a value_notification.central_scene trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, wallmote_central_scene)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -809,7 +864,10 @@ async def test_get_scene_activation_value_notification_triggers( ): """Test we get the expected triggers from a zwave_js device with the SceneActivation CC.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, hank_binary_switch)} + ) + assert device expected_trigger = { "platform": "device", "domain": DOMAIN, @@ -834,7 +892,10 @@ async def test_if_scene_activation_value_notification_fires( """Test for event.value_notification.scene_activation trigger firing.""" node: Node = hank_binary_switch dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, hank_binary_switch)} + ) + assert device assert await async_setup_component( hass, @@ -942,7 +1003,10 @@ async def test_get_trigger_capabilities_scene_activation_value_notification( ): """Test we get the expected capabilities from a value_notification.scene_activation trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, hank_binary_switch)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -977,7 +1041,10 @@ async def test_get_value_updated_value_triggers( ): """Test we get the zwave_js.value_updated.value trigger from a zwave_js device.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device expected_trigger = { "platform": "device", "domain": DOMAIN, @@ -997,7 +1064,10 @@ async def test_if_value_updated_value_fires( """Test for zwave_js.value_updated.value trigger firing.""" node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device assert await async_setup_component( hass, @@ -1080,12 +1150,86 @@ async def test_if_value_updated_value_fires( ) +async def test_value_updated_value_no_driver( + hass, client, lock_schlage_be469, integration, calls +): + """Test zwave_js.value_updated.value trigger with missing driver.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + driver = client.driver + client.driver = None + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "zwave_js.value_updated.value", + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + "property_key": None, + "endpoint": None, + "from": "open", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "zwave_js.value_updated.value - " + "{{ trigger.platform}} - " + "{{ trigger.previous_value }}" + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client.driver = driver + + # No trigger as automation failed to setup. + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "closed", + "prevValue": "open", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 0 + + async def test_get_trigger_capabilities_value_updated_value( hass, client, lock_schlage_be469, integration ): """Test we get the expected capabilities from a zwave_js.value_updated.value trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -1133,7 +1277,10 @@ async def test_get_value_updated_config_parameter_triggers( ): """Test we get the zwave_js.value_updated.config_parameter trigger from a zwave_js device.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device expected_trigger = { "platform": "device", "domain": DOMAIN, @@ -1158,7 +1305,10 @@ async def test_if_value_updated_config_parameter_fires( """Test for zwave_js.value_updated.config_parameter trigger firing.""" node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device assert await async_setup_component( hass, @@ -1225,7 +1375,10 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( ): """Test we get the expected capabilities from a range zwave_js.value_updated.config_parameter trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -1267,7 +1420,10 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_enumerate ): """Test we get the expected capabilities from an enumerated zwave_js.value_updated.config_parameter trigger.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device capabilities = await device_trigger.async_get_trigger_capabilities( hass, { @@ -1318,7 +1474,10 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration): ) dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, hank_binary_switch)} + ) + assert device with pytest.raises(HomeAssistantError): await device_trigger.async_attach_trigger( diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 8552f69936d..8201016b9d9 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -234,6 +234,8 @@ async def test_notifications(hass, hank_binary_switch, integration, client): async def test_value_updated(hass, vision_security_zl7432, integration, client): """Test value updated events.""" node = vision_security_zl7432 + # Add states to the value we are updating to ensure the translation happens + node.values["7-37-1-currentValue"].metadata.data["states"] = {"1": "on", "0": "off"} events = async_capture_events(hass, "zwave_js_value_updated") event = Event( @@ -266,7 +268,7 @@ async def test_value_updated(hass, vision_security_zl7432, integration, client): assert events[0].data["endpoint"] == 1 assert events[0].data["property_name"] == "currentValue" assert events[0].data["property"] == "currentValue" - assert events[0].data["value"] == 1 + assert events[0].data["value"] == "on" assert events[0].data["value_raw"] == 1 # Try a value updated event on a value we aren't watching to make sure diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 4f58c87febb..0d22484bea6 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -199,7 +199,7 @@ async def test_on_node_added_not_ready( device_id = f"{client.driver.controller.home_id}-{zp3111_not_ready_state['nodeId']}" assert len(hass.states.async_all()) == 0 - assert not dev_reg.devices + assert len(dev_reg.devices) == 1 node_state = deepcopy(zp3111_not_ready_state) node_state["isSecure"] = False @@ -911,12 +911,12 @@ async def test_removed_device( driver = client.driver assert driver # Verify how many nodes are available - assert len(driver.controller.nodes) == 2 + assert len(driver.controller.nodes) == 3 # Make sure there are the same number of devices dev_reg = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) - assert len(device_entries) == 2 + assert len(device_entries) == 3 # Check how many entities there are ent_reg = er.async_get(hass) @@ -931,7 +931,7 @@ async def test_removed_device( # Assert that the node and all of it's entities were removed from the device and # entity registry device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) - assert len(device_entries) == 1 + assert len(device_entries) == 2 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) assert len(entity_entries) == 18 assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 848c4d7b0e5..a32537b1d0d 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -156,9 +156,7 @@ async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration) assert entity_entry.disabled -async def test_node_status_sensor( - hass, client, controller_node, lock_id_lock_as_id150, integration -): +async def test_node_status_sensor(hass, client, lock_id_lock_as_id150, integration): """Test node status sensor is created and gets updated on node state changes.""" NODE_STATUS_ENTITY = "sensor.z_wave_module_for_id_lock_150_and_101_node_status" node = lock_id_lock_as_id150 diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 6e425bff042..710892c4741 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -34,10 +34,7 @@ from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.area_registry import async_get as async_get_area_reg -from homeassistant.helpers.device_registry import ( - async_entries_for_config_entry, - async_get as async_get_dev_reg, -) +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from homeassistant.setup import async_setup_component @@ -408,7 +405,8 @@ async def test_set_config_parameter_gather( async def test_bulk_set_config_parameters(hass, client, multisensor_6, integration): """Test the bulk_set_partial_config_parameters service.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) + assert device # Test setting config parameter by property and property_key await hass.services.async_call( DOMAIN, @@ -736,7 +734,10 @@ async def test_refresh_value( async def test_set_value(hass, client, climate_danfoss_lc_13, integration): """Test set_value service.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, climate_danfoss_lc_13)} + ) + assert device await hass.services.async_call( DOMAIN, diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index a5c226057d4..56a8e63b439 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -9,15 +9,13 @@ from zwave_js_server.model.node import Node from homeassistant.components import automation from homeassistant.components.zwave_js import DOMAIN +from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.components.zwave_js.trigger import async_validate_trigger_config from homeassistant.components.zwave_js.triggers.trigger_helpers import ( async_bypass_dynamic_config_validation, ) from homeassistant.const import SERVICE_RELOAD -from homeassistant.helpers.device_registry import ( - async_entries_for_config_entry, - async_get as async_get_dev_reg, -) +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.setup import async_setup_component from .common import SCHLAGE_BE469_LOCK_ENTITY @@ -30,7 +28,10 @@ async def test_zwave_js_value_updated(hass, client, lock_schlage_be469, integrat trigger_type = f"{DOMAIN}.value_updated" node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device no_value_filter = async_capture_events(hass, "no_value_filter") single_from_value_filter = async_capture_events(hass, "single_from_value_filter") @@ -385,12 +386,74 @@ async def test_zwave_js_value_updated_bypass_dynamic_validation_no_nodes( assert len(no_value_filter) == 0 +async def test_zwave_js_value_updated_bypass_dynamic_validation_no_driver( + hass, client, lock_schlage_be469, integration +): + """Test zwave_js.value_updated trigger without driver.""" + trigger_type = f"{DOMAIN}.value_updated" + node: Node = lock_schlage_be469 + driver = client.driver + client.driver = None + + no_value_filter = async_capture_events(hass, "no_value_filter") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # no value filter + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + "action": { + "event": "no_value_filter", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client.driver = driver + + # Test that no value filter is NOT triggered because automation failed setup + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "boo", + "prevValue": "hiss", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(no_value_filter) == 0 + + async def test_zwave_js_event(hass, client, lock_schlage_be469, integration): """Test for zwave_js.event automation trigger.""" trigger_type = f"{DOMAIN}.event" node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device node_no_event_data_filter = async_capture_events(hass, "node_no_event_data_filter") node_event_data_filter = async_capture_events(hass, "node_event_data_filter") @@ -933,7 +996,10 @@ async def test_zwave_js_trigger_config_entry_unloaded( ): """Test zwave_js triggers bypass dynamic validation when needed.""" dev_reg = async_get_dev_reg(hass) - device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device # Test bypass check is False assert not async_bypass_dynamic_config_validation( diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 4c00c1c9a3a..1650b7e8edd 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -72,7 +72,6 @@ async def test_update_entity_states( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, - controller_node, integration, caplog, hass_ws_client, diff --git a/tests/components/zwave_me/test_remove_stale_devices.py b/tests/components/zwave_me/test_remove_stale_devices.py new file mode 100644 index 00000000000..484c38b9f33 --- /dev/null +++ b/tests/components/zwave_me/test_remove_stale_devices.py @@ -0,0 +1,74 @@ +"""Test the zwave_me removal of stale devices.""" +from unittest.mock import patch +import uuid + +import pytest as pytest +from zwave_me_ws import ZWaveMeData + +from homeassistant.components.zwave_me import ZWaveMePlatform +from homeassistant.const import CONF_TOKEN, CONF_URL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, mock_device_registry + +DEFAULT_DEVICE_INFO = ZWaveMeData( + id="DummyDevice", + deviceType=ZWaveMePlatform.BINARY_SENSOR, + title="DeviceDevice", + level=100, + deviceIdentifier="16-23", +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +async def mock_connection(controller): + """Mock established connection and setting identifiers.""" + controller.on_new_device(DEFAULT_DEVICE_INFO) + return True + + +@pytest.mark.parametrize( + "identifier,should_exist", + [ + (DEFAULT_DEVICE_INFO.id, False), + (DEFAULT_DEVICE_INFO.deviceIdentifier, True), + ], +) +async def test_remove_stale_devices( + hass: HomeAssistant, device_reg, identifier, should_exist +): + """Test removing devices with old-format ids.""" + + config_entry = MockConfigEntry( + unique_id=uuid.uuid4(), + domain="zwave_me", + data={CONF_TOKEN: "test_token", CONF_URL: "http://test_test"}, + ) + config_entry.add_to_hass(hass) + device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + identifiers={("zwave_me", f"{config_entry.unique_id}-{identifier}")}, + ) + with patch("zwave_me_ws.ZWaveMe.get_connection", mock_connection,), patch( + "homeassistant.components.zwave_me.async_setup_platforms", + ): + await hass.config_entries.async_setup(config_entry.entry_id) + assert ( + bool( + device_reg.async_get_device( + { + ( + "zwave_me", + f"{config_entry.unique_id}-{identifier}", + ) + } + ) + ) + == should_exist + ) diff --git a/tests/conftest.py b/tests/conftest.py index 1047293ee16..4dc988db8fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,8 @@ import asyncio from collections.abc import AsyncGenerator, Callable, Generator from contextlib import asynccontextmanager import functools +import gc +import itertools from json import JSONDecoder, loads import logging import sqlite3 @@ -12,6 +14,7 @@ import ssl import threading from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch +import warnings from aiohttp import client from aiohttp.pytest_plugin import AiohttpClient @@ -187,12 +190,14 @@ util.get_local_ip = lambda: "127.0.0.1" @pytest.fixture(autouse=True) -def verify_cleanup(): +def verify_cleanup(event_loop: asyncio.AbstractEventLoop): """Verify that the test has cleaned up resources correctly.""" threads_before = frozenset(threading.enumerate()) - + tasks_before = asyncio.all_tasks(event_loop) yield + event_loop.run_until_complete(event_loop.shutdown_default_executor()) + if len(INSTANCES) >= 2: count = len(INSTANCES) for inst in INSTANCES: @@ -203,6 +208,26 @@ def verify_cleanup(): for thread in threads: assert isinstance(thread, threading._DummyThread) + # Warn and clean-up lingering tasks and timers + # before moving on to the next test. + tasks = asyncio.all_tasks(event_loop) - tasks_before + for task in tasks: + warnings.warn(f"Linger task after test {task}") + task.cancel() + if tasks: + event_loop.run_until_complete(asyncio.wait(tasks)) + + for handle in event_loop._scheduled: # pylint: disable=protected-access + if not handle.cancelled(): + warnings.warn(f"Lingering timer after test {handle}") + handle.cancel() + + # Make sure garbage collect run in same test as allocation + # this is to mimic the behavior of pytest-aiohttp, and is + # required to avoid warnings from spilling over into next + # test case. + gc.collect() + @pytest.fixture(autouse=True) def bcrypt_cost(): @@ -277,7 +302,7 @@ def aiohttp_client_cls(): @pytest.fixture def aiohttp_client( - loop: asyncio.AbstractEventLoop, + event_loop: asyncio.AbstractEventLoop, ) -> Generator[AiohttpClient, None, None]: """Override the default aiohttp_client since 3.x does not support aiohttp_client_cls. @@ -288,6 +313,7 @@ def aiohttp_client( aiohttp_client(server, **kwargs) aiohttp_client(raw_server, **kwargs) """ + loop = event_loop clients = [] async def go( @@ -334,9 +360,10 @@ def hass_fixture_setup(): @pytest.fixture -def hass(hass_fixture_setup, loop, load_registries, hass_storage, request): +def hass(hass_fixture_setup, event_loop, load_registries, hass_storage, request): """Fixture to provide a test instance of Home Assistant.""" + loop = event_loop hass_fixture_setup.append(True) orig_tz = dt_util.DEFAULT_TIME_ZONE @@ -381,7 +408,7 @@ def hass(hass_fixture_setup, loop, load_registries, hass_storage, request): @pytest.fixture -async def stop_hass(): +async def stop_hass(event_loop): """Make sure all hass are stopped.""" orig_hass = ha.HomeAssistant @@ -402,6 +429,7 @@ async def stop_hass(): with patch.object(hass_inst.loop, "stop"): await hass_inst.async_block_till_done() await hass_inst.async_stop(force=True) + await event_loop.shutdown_default_executor() @pytest.fixture @@ -860,6 +888,16 @@ def enable_statistics(): return False +@pytest.fixture +def enable_statistics_table_validation(): + """Fixture to control enabling of recorder's statistics table validation. + + To enable statistics table validation, tests can be marked with: + @pytest.mark.parametrize("enable_statistics_table_validation", [True]) + """ + return False + + @pytest.fixture def enable_nightly_purge(): """Fixture to control enabling of recorder's nightly purge job. @@ -902,6 +940,7 @@ def hass_recorder( recorder_db_url, enable_nightly_purge, enable_statistics, + enable_statistics_table_validation, hass_storage, ): """Home Assistant fixture with in-memory recorder.""" @@ -910,6 +949,11 @@ def hass_recorder( hass = get_test_home_assistant() nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None + stats_validate = ( + recorder.statistics.validate_db_schema + if enable_statistics_table_validation + else itertools.repeat(set()) + ) with patch( "homeassistant.components.recorder.Recorder.async_nightly_tasks", side_effect=nightly, @@ -918,6 +962,10 @@ def hass_recorder( "homeassistant.components.recorder.Recorder.async_periodic_statistics", side_effect=stats, autospec=True, + ), patch( + "homeassistant.components.recorder.migration.statistics_validate_db_schema", + side_effect=stats_validate, + autospec=True, ): def setup_recorder(config=None): @@ -962,12 +1010,18 @@ async def async_setup_recorder_instance( hass_fixture_setup, enable_nightly_purge, enable_statistics, + enable_statistics_table_validation, ) -> AsyncGenerator[SetupRecorderInstanceT, None]: """Yield callable to setup recorder instance.""" assert not hass_fixture_setup nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None + stats_validate = ( + recorder.statistics.validate_db_schema + if enable_statistics_table_validation + else itertools.repeat(set()) + ) with patch( "homeassistant.components.recorder.Recorder.async_nightly_tasks", side_effect=nightly, @@ -976,6 +1030,10 @@ async def async_setup_recorder_instance( "homeassistant.components.recorder.Recorder.async_periodic_statistics", side_effect=stats, autospec=True, + ), patch( + "homeassistant.components.recorder.migration.statistics_validate_db_schema", + side_effect=stats_validate, + autospec=True, ): async def async_setup_recorder( @@ -1041,18 +1099,19 @@ async def mock_enable_bluetooth( def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" with patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ), patch( - "bluetooth_adapters.BlueZDBusObjects", return_value=MagicMock(load=AsyncMock()) - ), patch( - "bluetooth_adapters.get_bluetooth_adapter_details", - return_value={ + "bluetooth_adapters.systems.platform.system", return_value="Linux" + ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { "hci0": { - "org.bluez.Adapter1": { - "Address": "00:00:00:00:00:01", - "Name": "BlueZ 4.63", - "Modalias": "usbid:1234", - } + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", }, }, ): diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index b0037aa3800..3e3536a2f42 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -2736,6 +2736,104 @@ "type": "HEATING_THERMOSTAT_COMPACT", "updateState": "UP_TO_DATE" }, + "3014F7110000000000000E70": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "1.0.10", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.10", + "firmwareVersionInteger": 65546, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000E70", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": 5, + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000012"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": "RIGHT", + "multicastRoutingEnabled": false, + "operationLockActive": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -64, + "rssiPeerValue": -67, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDisplayContrast": true, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": true + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000E70", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000013"], + "index": 1, + "label": "", + "setPointTemperature": 19.0, + "temperatureOffset": 0.5, + "valveActualTemperature": 18.7, + "valvePosition": 0.33, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000E70", + "label": "thermostat_evo", + "lastStatusUpdate": 1644406143910, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 425, + "modelType": "HmIP-eTRV-E", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000E70", + "type": "HEATING_THERMOSTAT_EVO", + "updateState": "UP_TO_DATE" + }, "3014F71100000000000TEST1": { "availableFirmwareVersion": "0.0.0", "connectionType": "HMIP_RF", @@ -6725,6 +6823,160 @@ "serializedGlobalTradeItemNumber": "3014F7110000000000STE2015", "type": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA", "updateState": "TRANSFERING_UPDATE" + }, + "3014F7110000000000000WGC": { + "availableFirmwareVersion": "1.0.2", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000WGC", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000011"], + "index": 0, + "label": "", + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -83, + "rssiPeerValue": -77, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": true, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000WGC", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F7110000000000000WGC", + "functionalChannelType": "IMPULSE_OUTPUT_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "impulseDuration": 0.4, + "index": 2, + "label": "Taste", + "processing": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000WGC", + "label": "Garagentor", + "lastStatusUpdate": 1630920800279, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 331, + "modelType": "HmIP-WGC", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000WGC", + "type": "WALL_MOUNTED_GARAGE_DOOR_CONTROLLER", + "updateState": "UP_TO_DATE" + }, + "HUE00000-0000-0000-0000-000000000008": { + "connectionType": "EXTERNAL", + "deviceArchetype": "EXTERNAL", + "externalService": "HUE", + "firmwareVersion": "1.88.1", + "functionalChannels": { + "0": { + "channelId": "HUE00000-0000-0000-0000-000000000009", + "deviceId": "HUE00000-0000-0000-0000-000000000008", + "functionalChannelType": "EXTERNAL_BASE_CHANNEL", + "groupIndex": 0, + "groups": ["HUE00000-0000-0000-0000-000000000010"], + "index": 0, + "label": "", + "unreach": false + }, + "1": { + "channelId": "HUE00000-0000-0000-0000-000000000011", + "channelRole": "UNIVERSAL_LIGHT_ACTUATOR", + "colorTemperature": 3165, + "deviceId": "HUE00000-0000-0000-0000-000000000008", + "dimLevel": 0.0, + "functionalChannelType": "EXTERNAL_UNIVERSAL_LIGHT_CHANNEL", + "groupIndex": 1, + "groups": ["HUE00000-0000-0000-0000-000000000012"], + "hue": null, + "index": 1, + "label": "", + "maximumColorTemperature": 6500, + "minimalColorTemperature": 2000, + "on": false, + "saturationLevel": null, + "supportedOptionalFeatures": { + "IFeatureLightGroupActuatorChannel": true, + "IOptionalFeatureColorTemperature": true, + "IOptionalFeatureDimmerState": true + } + } + }, + "hasCustomLabel": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "HUE00000-0000-0000-0000-000000000008", + "label": "Hinten rechts", + "lastStatusUpdate": 1669539365772, + "modelType": "LTW013", + "permanentlyReachable": true, + "supported": true, + "type": "EXTERNAL" } }, "groups": { diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index 7f12fb83fd7..eee184404b8 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -13,7 +13,7 @@ from script.hassfest.model import Integration def integration(): """Fixture for hassfest integration model.""" integration = Integration("") - integration.manifest = { + integration._manifest = { "domain": "test", "documentation": "https://example.com", "name": "test", diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index a5d2223a3d2..3b087b6a40b 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1332,3 +1332,39 @@ def test_currency(): for value in ("EUR", "USD"): assert schema(value) + + +def test_historic_currency(): + """Test historic currency validator.""" + schema = vol.Schema(cv.historic_currency) + + for value in (None, "BTC", "EUR"): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ("DEM", "NLG"): + assert schema(value) + + +def test_country(): + """Test country validator.""" + schema = vol.Schema(cv.country) + + for value in (None, "Candyland", "USA"): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ("NL", "SE"): + assert schema(value) + + +def test_language(): + """Test language validator.""" + schema = vol.Schema(cv.language) + + for value in (None, "Klingon", "english"): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ("en", "sv"): + assert schema(value) diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index f9d7ca47b4c..6a2012bb46e 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -186,11 +186,11 @@ def test_with_include_domain_glob_filtering_case4a_include_strong(): ) assert testfilter("sensor.working") - assert testfilter("sensor.notworking") is True # iclude is stronger + assert testfilter("sensor.notworking") is True # include is stronger assert testfilter("light.test") - assert testfilter("light.notworking") is True # iclude is stronger + assert testfilter("light.notworking") is True # include is stronger assert testfilter("light.ignoreme") is False - assert testfilter("binary_sensor.not_working") is True # iclude is stronger + assert testfilter("binary_sensor.not_working") is True # include is stronger assert testfilter("binary_sensor.another") is False assert testfilter("binary_sensor.specificly_included") is True assert testfilter("sun.sun") is False diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py deleted file mode 100644 index 2967b202efe..00000000000 --- a/tests/helpers/test_helper_config_entry_flow.py +++ /dev/null @@ -1,286 +0,0 @@ -"""Test schema_config_entry_flow.""" -import pytest -import voluptuous as vol - -from homeassistant import data_entry_flow -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.schema_config_entry_flow import ( - SchemaConfigFlowHandler, - SchemaFlowFormStep, - SchemaFlowMenuStep, - wrapped_entity_config_entry_title, -) -from homeassistant.util.decorator import Registry - -from tests.common import MockConfigEntry - - -@pytest.fixture -def manager(): - """Return a flow manager.""" - handlers = Registry() - entries = [] - - class FlowManager(data_entry_flow.FlowManager): - """Test flow manager.""" - - async def async_create_flow(self, handler_key, *, context, data): - """Test create flow.""" - handler = handlers.get(handler_key) - - if handler is None: - raise data_entry_flow.UnknownHandler - - flow = handler() - flow.init_step = context.get("init_step", "init") - return flow - - async def async_finish_flow(self, flow, result): - """Test finish flow.""" - if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - result["source"] = flow.context.get("source") - entries.append(result) - return result - - mgr = FlowManager(None) - mgr.mock_created_entries = entries - mgr.mock_reg_handler = handlers.register - return mgr - - -async def test_name(hass: HomeAssistant) -> None: - """Test the config flow name is copied from registry entry, with fallback to state.""" - registry = er.async_get(hass) - entity_id = "switch.ceiling" - - # No entry or state, use Object ID - assert wrapped_entity_config_entry_title(hass, entity_id) == "ceiling" - - # State set, use name from state - hass.states.async_set(entity_id, "on", {"friendly_name": "State Name"}) - assert wrapped_entity_config_entry_title(hass, entity_id) == "State Name" - - # Entity registered, use original name from registry entry - hass.states.async_remove(entity_id) - entry = registry.async_get_or_create( - "switch", - "test", - "unique", - suggested_object_id="ceiling", - original_name="Original Name", - ) - hass.states.async_set(entity_id, "on", {"friendly_name": "State Name"}) - assert entry.entity_id == entity_id - assert wrapped_entity_config_entry_title(hass, entity_id) == "Original Name" - assert wrapped_entity_config_entry_title(hass, entry.id) == "Original Name" - - # Entity has customized name - registry.async_update_entity("switch.ceiling", name="Custom Name") - assert wrapped_entity_config_entry_title(hass, entity_id) == "Custom Name" - assert wrapped_entity_config_entry_title(hass, entry.id) == "Custom Name" - - -@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) -async def test_config_flow_advanced_option( - hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker -): - """Test handling of advanced options in config flow.""" - manager.hass = hass - - CONFIG_SCHEMA = vol.Schema( - { - marker("option1"): str, - marker("advanced_no_default", description={"advanced": True}): str, - marker( - "advanced_default", - default="a very reasonable default", - description={"advanced": True}, - ): str, - } - ) - - CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(CONFIG_SCHEMA) - } - - @manager.mock_reg_handler("test") - class TestFlow(SchemaConfigFlowHandler): - config_flow = CONFIG_FLOW - - # Start flow in basic mode - result = await manager.async_init("test") - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert list(result["data_schema"].schema.keys()) == ["option1"] - - result = await manager.async_configure(result["flow_id"], {"option1": "blabla"}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == {} - assert result["options"] == { - "advanced_default": "a very reasonable default", - "option1": "blabla", - } - for option in result["options"]: - # Make sure we didn't get the Optional or Required instance as key - assert isinstance(option, str) - - # Start flow in advanced mode - result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert list(result["data_schema"].schema.keys()) == [ - "option1", - "advanced_no_default", - "advanced_default", - ] - - result = await manager.async_configure( - result["flow_id"], {"advanced_no_default": "abc123", "option1": "blabla"} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == {} - assert result["options"] == { - "advanced_default": "a very reasonable default", - "advanced_no_default": "abc123", - "option1": "blabla", - } - for option in result["options"]: - # Make sure we didn't get the Optional or Required instance as key - assert isinstance(option, str) - - # Start flow in advanced mode - result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert list(result["data_schema"].schema.keys()) == [ - "option1", - "advanced_no_default", - "advanced_default", - ] - - result = await manager.async_configure( - result["flow_id"], - { - "advanced_default": "not default", - "advanced_no_default": "abc123", - "option1": "blabla", - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == {} - assert result["options"] == { - "advanced_default": "not default", - "advanced_no_default": "abc123", - "option1": "blabla", - } - for option in result["options"]: - # Make sure we didn't get the Optional or Required instance as key - assert isinstance(option, str) - - -@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) -async def test_options_flow_advanced_option( - hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker -): - """Test handling of advanced options in options flow.""" - manager.hass = hass - - OPTIONS_SCHEMA = vol.Schema( - { - marker("option1"): str, - marker("advanced_no_default", description={"advanced": True}): str, - marker( - "advanced_default", - default="a very reasonable default", - description={"advanced": True}, - ): str, - } - ) - - OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA) - } - - class TestFlow(SchemaConfigFlowHandler, domain="test"): - config_flow = {} - options_flow = OPTIONS_FLOW - - config_entry = MockConfigEntry( - data={}, - domain="test", - options={ - "option1": "blabla", - "advanced_no_default": "abc123", - "advanced_default": "not default", - }, - ) - config_entry.add_to_hass(hass) - - # Start flow in basic mode - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert list(result["data_schema"].schema.keys()) == ["option1"] - - result = await hass.config_entries.options.async_configure( - result["flow_id"], {"option1": "blublu"} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { - "advanced_default": "not default", - "advanced_no_default": "abc123", - "option1": "blublu", - } - for option in result["data"]: - # Make sure we didn't get the Optional or Required instance as key - assert isinstance(option, str) - - # Start flow in advanced mode - result = await hass.config_entries.options.async_init( - config_entry.entry_id, context={"show_advanced_options": True} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert list(result["data_schema"].schema.keys()) == [ - "option1", - "advanced_no_default", - "advanced_default", - ] - - result = await hass.config_entries.options.async_configure( - result["flow_id"], {"advanced_no_default": "def456", "option1": "blabla"} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { - "advanced_default": "a very reasonable default", - "advanced_no_default": "def456", - "option1": "blabla", - } - for option in result["data"]: - # Make sure we didn't get the Optional or Required instance as key - assert isinstance(option, str) - - # Start flow in advanced mode - result = await hass.config_entries.options.async_init( - config_entry.entry_id, context={"show_advanced_options": True} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert list(result["data_schema"].schema.keys()) == [ - "option1", - "advanced_no_default", - "advanced_default", - ] - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - { - "advanced_default": "also not default", - "advanced_no_default": "abc123", - "option1": "blabla", - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { - "advanced_default": "also not default", - "advanced_no_default": "abc123", - "option1": "blabla", - } - for option in result["data"]: - # Make sure we didn't get the Optional or Required instance as key - assert isinstance(option, str) diff --git a/tests/helpers/test_schema_config_entry_flow.py b/tests/helpers/test_schema_config_entry_flow.py new file mode 100644 index 00000000000..b01da8d3d2a --- /dev/null +++ b/tests/helpers/test_schema_config_entry_flow.py @@ -0,0 +1,649 @@ +"""Tests for the schema based data entry flows.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +import pytest +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, + SchemaFlowMenuStep, + SchemaOptionsFlowHandler, + wrapped_entity_config_entry_title, +) +from homeassistant.util.decorator import Registry + +from tests.common import MockConfigEntry, mock_platform + +TEST_DOMAIN = "test" + + +@pytest.fixture(name="manager") +def manager_fixture(): + """Return a flow manager.""" + handlers = Registry() + entries = [] + + class FlowManager(data_entry_flow.FlowManager): + """Test flow manager.""" + + async def async_create_flow(self, handler_key, *, context, data): + """Test create flow.""" + handler = handlers.get(handler_key) + + if handler is None: + raise data_entry_flow.UnknownHandler + + flow = handler() + flow.init_step = context.get("init_step", "init") + return flow + + async def async_finish_flow(self, flow, result): + """Test finish flow.""" + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: + result["source"] = flow.context.get("source") + entries.append(result) + return result + + mgr = FlowManager(None) + mgr.mock_created_entries = entries + mgr.mock_reg_handler = handlers.register + return mgr + + +async def test_name(hass: HomeAssistant) -> None: + """Test the config flow name is copied from registry entry, with fallback to state.""" + registry = er.async_get(hass) + entity_id = "switch.ceiling" + + # No entry or state, use Object ID + assert wrapped_entity_config_entry_title(hass, entity_id) == "ceiling" + + # State set, use name from state + hass.states.async_set(entity_id, "on", {"friendly_name": "State Name"}) + assert wrapped_entity_config_entry_title(hass, entity_id) == "State Name" + + # Entity registered, use original name from registry entry + hass.states.async_remove(entity_id) + entry = registry.async_get_or_create( + "switch", + "test", + "unique", + suggested_object_id="ceiling", + original_name="Original Name", + ) + hass.states.async_set(entity_id, "on", {"friendly_name": "State Name"}) + assert entry.entity_id == entity_id + assert wrapped_entity_config_entry_title(hass, entity_id) == "Original Name" + assert wrapped_entity_config_entry_title(hass, entry.id) == "Original Name" + + # Entity has customized name + registry.async_update_entity("switch.ceiling", name="Custom Name") + assert wrapped_entity_config_entry_title(hass, entity_id) == "Custom Name" + assert wrapped_entity_config_entry_title(hass, entry.id) == "Custom Name" + + +@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +async def test_config_flow_advanced_option( + hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker +): + """Test handling of advanced options in config flow.""" + manager.hass = hass + + CONFIG_SCHEMA = vol.Schema( + { + marker("option1"): str, + marker("advanced_no_default", description={"advanced": True}): str, + marker( + "advanced_default", + default="a very reasonable default", + description={"advanced": True}, + ): str, + } + ) + + CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(CONFIG_SCHEMA) + } + + @manager.mock_reg_handler("test") + class TestFlow(SchemaConfigFlowHandler): + config_flow = CONFIG_FLOW + + # Start flow in basic mode + result = await manager.async_init("test") + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert list(result["data_schema"].schema.keys()) == ["option1"] + + result = await manager.async_configure(result["flow_id"], {"option1": "blabla"}) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "a very reasonable default", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await manager.async_init("test", context={"show_advanced_options": True}) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await manager.async_configure( + result["flow_id"], {"advanced_no_default": "abc123", "option1": "blabla"} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "a very reasonable default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await manager.async_init("test", context={"show_advanced_options": True}) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await manager.async_configure( + result["flow_id"], + { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blabla", + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + +@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +async def test_options_flow_advanced_option( + hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker +): + """Test handling of advanced options in options flow.""" + manager.hass = hass + + OPTIONS_SCHEMA = vol.Schema( + { + marker("option1"): str, + marker("advanced_no_default", description={"advanced": True}): str, + marker( + "advanced_default", + default="a very reasonable default", + description={"advanced": True}, + ): str, + } + ) + + OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA) + } + + class TestFlow(SchemaConfigFlowHandler, domain="test"): + config_flow = {} + options_flow = OPTIONS_FLOW + + config_entry = MockConfigEntry( + data={}, + domain="test", + options={ + "option1": "blabla", + "advanced_no_default": "abc123", + "advanced_default": "not default", + }, + ) + config_entry.add_to_hass(hass) + + # Start flow in basic mode + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert list(result["data_schema"].schema.keys()) == ["option1"] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blublu"} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blublu", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"advanced_no_default": "def456", "option1": "blabla"} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + "advanced_default": "a very reasonable default", + "advanced_no_default": "def456", + "option1": "blabla", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { + "advanced_default": "also not default", + "advanced_no_default": "abc123", + "option1": "blabla", + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + "advanced_default": "also not default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + +async def test_menu_step(hass: HomeAssistant) -> None: + """Test menu step.""" + + MENU_1 = ["option1", "option2"] + MENU_2 = ["option3", "option4"] + + async def _option1_next_step(_: dict[str, Any]) -> str: + return "menu2" + + CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowMenuStep(MENU_1), + "option1": SchemaFlowFormStep(vol.Schema({}), next_step=_option1_next_step), + "menu2": SchemaFlowMenuStep(MENU_2), + "option3": SchemaFlowFormStep(vol.Schema({}), next_step="option4"), + "option4": SchemaFlowFormStep(vol.Schema({})), + } + + class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN): + """Handle a config or options flow for Derivative.""" + + config_flow = CONFIG_FLOW + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestConfigFlow}): + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "user"} + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "option1"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "option1" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "menu2" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "option3"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "option3" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "option4" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_schema_none(hass: HomeAssistant) -> None: + """Test SchemaFlowFormStep with schema set to None.""" + + CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(next_step="option1"), + "option1": SchemaFlowFormStep(vol.Schema({}), next_step="pass"), + "pass": SchemaFlowFormStep(next_step="option3"), + "option3": SchemaFlowFormStep(vol.Schema({})), + } + + class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN): + """Handle a config or options flow for Derivative.""" + + config_flow = CONFIG_FLOW + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestConfigFlow}): + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "user"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "option1" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "option3" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_last_step(hass: HomeAssistant) -> None: + """Test SchemaFlowFormStep with schema set to None.""" + + async def _step2_next_step(_: dict[str, Any]) -> str: + return "step3" + + CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(next_step="step1"), + "step1": SchemaFlowFormStep(vol.Schema({}), next_step="step2"), + "step2": SchemaFlowFormStep(vol.Schema({}), next_step=_step2_next_step), + "step3": SchemaFlowFormStep(vol.Schema({}), next_step=None), + } + + class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN): + """Handle a config or options flow for Derivative.""" + + config_flow = CONFIG_FLOW + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestConfigFlow}): + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "user"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "step1" + assert result["last_step"] is False + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "step2" + assert result["last_step"] is None + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "step3" + assert result["last_step"] is True + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_next_step_function(hass: HomeAssistant) -> None: + """Test SchemaFlowFormStep with a next_step function.""" + + async def _step1_next_step(_: dict[str, Any]) -> str: + return "step2" + + async def _step2_next_step(_: dict[str, Any]) -> None: + return None + + CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(next_step="step1"), + "step1": SchemaFlowFormStep(vol.Schema({}), next_step=_step1_next_step), + "step2": SchemaFlowFormStep(vol.Schema({}), next_step=_step2_next_step), + } + + class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN): + """Handle a config or options flow for Derivative.""" + + config_flow = CONFIG_FLOW + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestConfigFlow}): + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": "user"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "step1" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "step2" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_suggested_values( + hass: HomeAssistant, manager: data_entry_flow.FlowManager +) -> None: + """Test suggested_values handling in SchemaFlowFormStep.""" + manager.hass = hass + + OPTIONS_SCHEMA = vol.Schema( + {vol.Optional("option1", default="a very reasonable default"): str} + ) + + async def _validate_user_input( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] + ) -> dict[str, Any]: + if user_input["option1"] == "not a valid value": + raise SchemaFlowError("option1 not using a valid value") + return user_input + + async def _step_2_suggested_values(_: SchemaCommonFlowHandler) -> dict[str, Any]: + return {"option1": "a random override"} + + OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA, next_step="step_1"), + "step_1": SchemaFlowFormStep(OPTIONS_SCHEMA, next_step="step_2"), + "step_2": SchemaFlowFormStep( + OPTIONS_SCHEMA, + suggested_values=_step_2_suggested_values, + next_step="step_3", + ), + "step_3": SchemaFlowFormStep( + OPTIONS_SCHEMA, suggested_values=None, next_step="step_4" + ), + "step_4": SchemaFlowFormStep( + OPTIONS_SCHEMA, validate_user_input=_validate_user_input + ), + } + + class TestFlow(SchemaConfigFlowHandler, domain="test"): + config_flow = {} + options_flow = OPTIONS_FLOW + + config_entry = MockConfigEntry( + data={}, + domain="test", + options={"option1": "initial value"}, + ) + config_entry.add_to_hass(hass) + + # Start flow in basic mode, suggested values should be the existing options + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + assert schema_keys == ["option1"] + assert schema_keys[0].description == {"suggested_value": "initial value"} + + # Go to step 1, suggested values should be the input from init + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blublu"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "step_1" + schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + assert schema_keys == ["option1"] + assert schema_keys[0].description == {"suggested_value": "blublu"} + + # Go to step 2, suggested values should come from the callback function + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blabla"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "step_2" + schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + assert schema_keys == ["option1"] + assert schema_keys[0].description == {"suggested_value": "a random override"} + + # Go to step 3, suggested values should be empty + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blabla"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "step_3" + schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + assert schema_keys == ["option1"] + assert schema_keys[0].description is None + + # Go to step 4, suggested values should be the user input + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blabla"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "step_4" + schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + assert schema_keys == ["option1"] + assert schema_keys[0].description == {"suggested_value": "blabla"} + + # Incorrect value in step 4, suggested values should be the user input + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "not a valid value"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "step_4" + schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + assert schema_keys == ["option1"] + assert schema_keys[0].description == {"suggested_value": "not a valid value"} + + # Correct value in step 4, end of flow + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blabla"} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + +async def test_options_flow_state(hass: HomeAssistant) -> None: + """Test flow_state handling in SchemaFlowFormStep.""" + + OPTIONS_SCHEMA = vol.Schema( + {vol.Optional("option1", default="a very reasonable default"): str} + ) + + async def _init_schema(handler: SchemaCommonFlowHandler) -> None: + handler.flow_state["idx"] = None + + async def _validate_step1_input( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] + ) -> dict[str, Any]: + handler.flow_state["idx"] = user_input["option1"] + return user_input + + async def _validate_step2_input( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] + ) -> dict[str, Any]: + user_input["idx_from_flow_state"] = handler.flow_state["idx"] + return user_input + + OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(_init_schema, next_step="step_1"), + "step_1": SchemaFlowFormStep( + OPTIONS_SCHEMA, + validate_user_input=_validate_step1_input, + next_step="step_2", + ), + "step_2": SchemaFlowFormStep( + OPTIONS_SCHEMA, + validate_user_input=_validate_step2_input, + ), + } + + class TestFlow(SchemaConfigFlowHandler, domain="test"): + config_flow = {} + options_flow = OPTIONS_FLOW + + config_entry = MockConfigEntry( + data={}, + domain="test", + options={"option1": "initial value"}, + ) + config_entry.add_to_hass(hass) + + # Start flow in basic mode, flow state is initialised with None value + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "step_1" + + options_handler: SchemaOptionsFlowHandler + options_handler = hass.config_entries.options._progress[result["flow_id"]] + assert options_handler._common_handler.flow_state == {"idx": None} + + # In step 1, flow state is updated with user input + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blublu"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "step_2" + + options_handler = hass.config_entries.options._progress[result["flow_id"]] + assert options_handler._common_handler.flow_state == {"idx": "blublu"} + + # In step 2, options were updated from flow state + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blabla"} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + "idx_from_flow_state": "blublu", + "option1": "blabla", + } diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 519498f2e08..a5f3cc0cc91 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -8,7 +8,7 @@ import logging import operator from types import MappingProxyType from unittest import mock -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from async_timeout import timeout import pytest @@ -1734,6 +1734,7 @@ async def test_condition_created_once(async_from_config, hass): ) async_from_config.reset_mock() + async_from_config.return_value = MagicMock() hass.states.async_set("test.entity", "hello") await script_obj.async_run(context=Context()) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index ca5cb92bfd5..0fcfbc46ef9 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -394,13 +394,13 @@ async def test_migration(hass, hass_storage, store_v_1_2): } assert calls == 0 - legacy_store = CustomStore(hass, 2, store_v_1_2.key, minor_version=1) - data = await legacy_store.async_load() + custom_store = CustomStore(hass, 2, store_v_1_2.key, minor_version=1) + data = await custom_store.async_load() assert calls == 1 assert hass_storage[store_v_1_2.key]["data"] == data - await legacy_store.async_save(MOCK_DATA) - assert hass_storage[legacy_store.key] == { + # Assert the migrated data has been saved + assert hass_storage[custom_store.key] == { "key": MOCK_KEY, "version": 2, "minor_version": 1, @@ -433,7 +433,7 @@ async def test_legacy_migration(hass, hass_storage, store_v_1_2): assert calls == 1 assert hass_storage[store_v_1_2.key]["data"] == data - await legacy_store.async_save(MOCK_DATA) + # Assert the migrated data has been saved assert hass_storage[legacy_store.key] == { "key": MOCK_KEY, "version": 2, diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 63a6154a190..e19daf00627 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,8 +1,12 @@ """Test Home Assistant template helper methods.""" +from __future__ import annotations + +from collections.abc import Iterable from datetime import datetime, timedelta import logging import math import random +from typing import Any from unittest.mock import patch from freezegun import freeze_time @@ -27,6 +31,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import device_registry as dr, entity, template from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.json import json_dumps +from homeassistant.helpers.typing import TemplateVarsType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem @@ -39,7 +44,7 @@ from tests.common import ( ) -def _set_up_units(hass): +def _set_up_units(hass: HomeAssistant) -> None: """Set up the tests.""" hass.config.units = UnitSystem( "custom", @@ -54,25 +59,37 @@ def _set_up_units(hass): ) -def render(hass, template_str, variables=None): +def render( + hass: HomeAssistant, template_str: str, variables: TemplateVarsType | None = None +) -> Any: """Create render info from template.""" tmp = template.Template(template_str, hass) return tmp.async_render(variables) -def render_to_info(hass, template_str, variables=None): +def render_to_info( + hass: HomeAssistant, template_str: str, variables: TemplateVarsType | None = None +) -> template.RenderInfo: """Create render info from template.""" tmp = template.Template(template_str, hass) return tmp.async_render_to_info(variables) -def extract_entities(hass, template_str, variables=None): +def extract_entities( + hass: HomeAssistant, template_str: str, variables: TemplateVarsType | None = None +) -> set[str]: """Extract entities from a template.""" info = render_to_info(hass, template_str, variables) return info.entities -def assert_result_info(info, result, entities=None, domains=None, all_states=False): +def assert_result_info( + info: template.RenderInfo, + result: Any, + entities: Iterable[str] | None = None, + domains: Iterable[str] | None = None, + all_states: bool = False, +) -> None: """Check result info.""" assert info.result() == result assert info.all_states == all_states @@ -91,7 +108,7 @@ def assert_result_info(info, result, entities=None, domains=None, all_states=Fal assert not hasattr(info, "_domains") -def test_template_equality(): +def test_template_equality() -> None: """Test template comparison and hashing.""" template_one = template.Template("{{ template_one }}") template_one_1 = template.Template("{{ template_one }}") @@ -108,7 +125,7 @@ def test_template_equality(): template.Template(["{{ template_one }}"]) -def test_invalid_template(hass): +def test_invalid_template(hass: HomeAssistant) -> None: """Invalid template raises error.""" tmpl = template.Template("{{", hass) @@ -130,7 +147,7 @@ def test_invalid_template(hass): tmpl.async_render() -def test_referring_states_by_entity_id(hass): +def test_referring_states_by_entity_id(hass: HomeAssistant) -> None: """Test referring states by entity id.""" hass.states.async_set("test.object", "happy") assert ( @@ -148,7 +165,7 @@ def test_referring_states_by_entity_id(hass): ) -def test_invalid_entity_id(hass): +def test_invalid_entity_id(hass: HomeAssistant) -> None: """Test referring states by entity id.""" with pytest.raises(TemplateError): template.Template('{{ states["big.fat..."] }}', hass).async_render() @@ -158,13 +175,13 @@ def test_invalid_entity_id(hass): template.Template('{{ states["invalid/domain"] }}', hass).async_render() -def test_raise_exception_on_error(hass): +def test_raise_exception_on_error(hass: HomeAssistant) -> None: """Test raising an exception on error.""" with pytest.raises(TemplateError): template.Template("{{ invalid_syntax").ensure_valid() -def test_iterating_all_states(hass): +def test_iterating_all_states(hass: HomeAssistant) -> None: """Test iterating all states.""" tmpl_str = "{% for state in states %}{{ state.state }}{% endfor %}" @@ -179,7 +196,7 @@ def test_iterating_all_states(hass): assert_result_info(info, "10happy", entities=[], all_states=True) -def test_iterating_all_states_unavailable(hass): +def test_iterating_all_states_unavailable(hass: HomeAssistant) -> None: """Test iterating all states unavailable.""" hass.states.async_set("test.object", "on") @@ -197,7 +214,7 @@ def test_iterating_all_states_unavailable(hass): assert_result_info(info, 1, entities=[], all_states=True) -def test_iterating_domain_states(hass): +def test_iterating_domain_states(hass: HomeAssistant) -> None: """Test iterating domain states.""" tmpl_str = "{% for state in states.sensor %}{{ state.state }}{% endfor %}" @@ -218,7 +235,7 @@ def test_iterating_domain_states(hass): ) -def test_float_function(hass): +def test_float_function(hass: HomeAssistant) -> None: """Test float function.""" hass.states.async_set("sensor.temperature", "12") @@ -245,7 +262,7 @@ def test_float_function(hass): assert render(hass, "{{ float('bad', default=1) }}") == 1 -def test_float_filter(hass): +def test_float_filter(hass: HomeAssistant) -> None: """Test float filter.""" hass.states.async_set("sensor.temperature", "12") @@ -261,7 +278,7 @@ def test_float_filter(hass): assert render(hass, "{{ 'bad' | float(default=1) }}") == 1 -def test_int_filter(hass): +def test_int_filter(hass: HomeAssistant) -> None: """Test int filter.""" hass.states.async_set("sensor.temperature", "12.2") assert render(hass, "{{ states.sensor.temperature.state | int }}") == 12 @@ -279,7 +296,7 @@ def test_int_filter(hass): assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 -def test_int_function(hass): +def test_int_function(hass: HomeAssistant) -> None: """Test int filter.""" hass.states.async_set("sensor.temperature", "12.2") assert render(hass, "{{ int(states.sensor.temperature.state) }}") == 12 @@ -297,7 +314,7 @@ def test_int_function(hass): assert render(hass, "{{ int('bad', default=1) }}") == 1 -def test_bool_function(hass): +def test_bool_function(hass: HomeAssistant) -> None: """Test bool function.""" assert render(hass, "{{ bool(true) }}") is True assert render(hass, "{{ bool(false) }}") is False @@ -311,7 +328,7 @@ def test_bool_function(hass): assert render(hass, "{{ bool('unavailable', default=none) }}") is None -def test_bool_filter(hass): +def test_bool_filter(hass: HomeAssistant) -> None: """Test bool filter.""" assert render(hass, "{{ true | bool }}") is True assert render(hass, "{{ false | bool }}") is False @@ -366,7 +383,7 @@ def test_isnumber(hass, value, expected): ) -def test_rounding_value(hass): +def test_rounding_value(hass: HomeAssistant) -> None: """Test rounding value.""" hass.states.async_set("sensor.temperature", 12.78) @@ -406,7 +423,7 @@ def test_rounding_value(hass): ) -def test_rounding_value_on_error(hass): +def test_rounding_value_on_error(hass: HomeAssistant) -> None: """Test rounding value handling of error.""" # Test handling of invalid input with pytest.raises(TemplateError): @@ -419,7 +436,7 @@ def test_rounding_value_on_error(hass): assert render(hass, "{{ 'no_number' | round(default=1) }}") == 1 -def test_multiply(hass): +def test_multiply(hass: HomeAssistant) -> None: """Test multiply.""" tests = {10: 100} @@ -440,7 +457,7 @@ def test_multiply(hass): assert render(hass, "{{ 'no_number' | multiply(10, default=1) }}") == 1 -def test_logarithm(hass): +def test_logarithm(hass: HomeAssistant) -> None: """Test logarithm.""" tests = [ (4, 2, 2.0), @@ -482,7 +499,7 @@ def test_logarithm(hass): assert render(hass, "{{ log(0, 10, default=1) }}") == 1 -def test_sine(hass): +def test_sine(hass: HomeAssistant) -> None: """Test sine.""" tests = [ (0, 0.0), @@ -512,7 +529,7 @@ def test_sine(hass): assert render(hass, "{{ sin('no_number', default=1) }}") == 1 -def test_cos(hass): +def test_cos(hass: HomeAssistant) -> None: """Test cosine.""" tests = [ (0, 1.0), @@ -542,7 +559,7 @@ def test_cos(hass): assert render(hass, "{{ cos('no_number', default=1) }}") == 1 -def test_tan(hass): +def test_tan(hass: HomeAssistant) -> None: """Test tangent.""" tests = [ (0, 0.0), @@ -572,7 +589,7 @@ def test_tan(hass): assert render(hass, "{{ tan('no_number', default=1) }}") == 1 -def test_sqrt(hass): +def test_sqrt(hass: HomeAssistant) -> None: """Test square root.""" tests = [ (0, 0.0), @@ -602,7 +619,7 @@ def test_sqrt(hass): assert render(hass, "{{ sqrt('no_number', default=1) }}") == 1 -def test_arc_sine(hass): +def test_arc_sine(hass: HomeAssistant) -> None: """Test arcus sine.""" tests = [ (-1.0, -1.571), @@ -639,7 +656,7 @@ def test_arc_sine(hass): assert render(hass, "{{ asin('no_number', default=1) }}") == 1 -def test_arc_cos(hass): +def test_arc_cos(hass: HomeAssistant) -> None: """Test arcus cosine.""" tests = [ (-1.0, 3.142), @@ -676,7 +693,7 @@ def test_arc_cos(hass): assert render(hass, "{{ acos('no_number', default=1) }}") == 1 -def test_arc_tan(hass): +def test_arc_tan(hass: HomeAssistant) -> None: """Test arcus tangent.""" tests = [ (-10.0, -1.471), @@ -710,7 +727,7 @@ def test_arc_tan(hass): assert render(hass, "{{ atan('no_number', default=1) }}") == 1 -def test_arc_tan2(hass): +def test_arc_tan2(hass: HomeAssistant) -> None: """Test two parameter version of arcus tangent.""" tests = [ (-10.0, -10.0, -2.356), @@ -754,7 +771,7 @@ def test_arc_tan2(hass): assert render(hass, "{{ atan2('duck', 'goose', default=1) }}") == 1 -def test_strptime(hass): +def test_strptime(hass: HomeAssistant) -> None: """Test the parse timestamp method.""" tests = [ ("2016-10-19 15:22:05.588122 UTC", "%Y-%m-%d %H:%M:%S.%f %Z", None), @@ -790,7 +807,7 @@ def test_strptime(hass): assert render(hass, "{{ strptime('invalid', '%Y', default=1) }}") == 1 -def test_timestamp_custom(hass): +def test_timestamp_custom(hass: HomeAssistant) -> None: """Test the timestamps to custom filter.""" hass.config.set_time_zone("UTC") now = dt_util.utcnow() @@ -832,7 +849,7 @@ def test_timestamp_custom(hass): assert render(hass, "{{ None | timestamp_custom(default=1) }}") == 1 -def test_timestamp_local(hass): +def test_timestamp_local(hass: HomeAssistant) -> None: """Test the timestamps to local filter.""" hass.config.set_time_zone("UTC") tests = [ @@ -886,7 +903,7 @@ def test_as_datetime(hass, input): ) -def test_as_datetime_from_timestamp(hass): +def test_as_datetime_from_timestamp(hass: HomeAssistant) -> None: """Test converting a UNIX timestamp to a date object.""" tests = [ (1469119144, "2016-07-21 16:39:04+00:00"), @@ -916,7 +933,7 @@ def test_as_datetime_from_timestamp(hass): ) -def test_as_local(hass): +def test_as_local(hass: HomeAssistant) -> None: """Test converting time to local.""" hass.states.async_set("test.object", "available") @@ -929,7 +946,7 @@ def test_as_local(hass): ).async_render() == str(dt_util.as_local(last_updated)) -def test_to_json(hass): +def test_to_json(hass: HomeAssistant) -> None: """Test the object to JSON string filter.""" # Note that we're not testing the actual json.loads and json.dumps methods, @@ -941,7 +958,7 @@ def test_to_json(hass): assert actual_result == expected_result -def test_to_json_string(hass): +def test_to_json_string(hass: HomeAssistant) -> None: """Test the object to JSON string filter.""" # Note that we're not testing the actual json.loads and json.dumps methods, @@ -956,7 +973,7 @@ def test_to_json_string(hass): assert actual_value == '"Bar ҝ éèà"' -def test_from_json(hass): +def test_from_json(hass: HomeAssistant) -> None: """Test the JSON string to object filter.""" # Note that we're not testing the actual json.loads and json.dumps methods, @@ -968,7 +985,7 @@ def test_from_json(hass): assert actual_result == expected_result -def test_average(hass): +def test_average(hass: HomeAssistant) -> None: """Test the average filter.""" assert template.Template("{{ [1, 2, 3] | average }}", hass).async_render() == 2 assert template.Template("{{ average([1, 2, 3]) }}", hass).async_render() == 2 @@ -996,7 +1013,7 @@ def test_average(hass): template.Template("{{ average([]) }}", hass).async_render() -def test_min(hass): +def test_min(hass: HomeAssistant) -> None: """Test the min filter.""" assert template.Template("{{ [1, 2, 3] | min }}", hass).async_render() == 1 assert template.Template("{{ min([1, 2, 3]) }}", hass).async_render() == 1 @@ -1012,7 +1029,7 @@ def test_min(hass): template.Template("{{ min(1) }}", hass).async_render() -def test_max(hass): +def test_max(hass: HomeAssistant) -> None: """Test the max filter.""" assert template.Template("{{ [1, 2, 3] | max }}", hass).async_render() == 3 assert template.Template("{{ max([1, 2, 3]) }}", hass).async_render() == 3 @@ -1095,12 +1112,12 @@ def test_min_max_attribute(hass, attribute): ) -def test_ord(hass): +def test_ord(hass: HomeAssistant) -> None: """Test the ord filter.""" assert template.Template('{{ "d" | ord }}', hass).async_render() == 100 -def test_base64_encode(hass): +def test_base64_encode(hass: HomeAssistant) -> None: """Test the base64_encode filter.""" assert ( template.Template('{{ "homeassistant" | base64_encode }}', hass).async_render() @@ -1108,7 +1125,7 @@ def test_base64_encode(hass): ) -def test_base64_decode(hass): +def test_base64_decode(hass: HomeAssistant) -> None: """Test the base64_decode filter.""" assert ( template.Template( @@ -1118,7 +1135,7 @@ def test_base64_decode(hass): ) -def test_slugify(hass): +def test_slugify(hass: HomeAssistant) -> None: """Test the slugify filter.""" assert ( template.Template('{{ slugify("Home Assistant") }}', hass).async_render() @@ -1138,7 +1155,7 @@ def test_slugify(hass): ) -def test_ordinal(hass): +def test_ordinal(hass: HomeAssistant) -> None: """Test the ordinal filter.""" tests = [ (1, "1st"), @@ -1158,7 +1175,7 @@ def test_ordinal(hass): ) -def test_timestamp_utc(hass): +def test_timestamp_utc(hass: HomeAssistant) -> None: """Test the timestamps to local filter.""" now = dt_util.utcnow() tests = [ @@ -1186,7 +1203,7 @@ def test_timestamp_utc(hass): assert render(hass, "{{ None | timestamp_utc(default=1) }}") == 1 -def test_as_timestamp(hass): +def test_as_timestamp(hass: HomeAssistant) -> None: """Test the as_timestamp function.""" with pytest.raises(TemplateError): template.Template('{{ as_timestamp("invalid") }}', hass).async_render() @@ -1218,24 +1235,24 @@ def test_random_every_time(test_choice, hass): assert tpl.async_render() == "bar" -def test_passing_vars_as_keywords(hass): +def test_passing_vars_as_keywords(hass: HomeAssistant) -> None: """Test passing variables as keywords.""" assert template.Template("{{ hello }}", hass).async_render(hello=127) == 127 -def test_passing_vars_as_vars(hass): +def test_passing_vars_as_vars(hass: HomeAssistant) -> None: """Test passing variables as variables.""" assert template.Template("{{ hello }}", hass).async_render({"hello": 127}) == 127 -def test_passing_vars_as_list(hass): +def test_passing_vars_as_list(hass: HomeAssistant) -> None: """Test passing variables as list.""" assert template.render_complex( template.Template("{{ hello }}", hass), {"hello": ["foo", "bar"]} ) == ["foo", "bar"] -def test_passing_vars_as_list_element(hass): +def test_passing_vars_as_list_element(hass: HomeAssistant) -> None: """Test passing variables as list.""" assert ( template.render_complex( @@ -1245,7 +1262,7 @@ def test_passing_vars_as_list_element(hass): ) -def test_passing_vars_as_dict_element(hass): +def test_passing_vars_as_dict_element(hass: HomeAssistant) -> None: """Test passing variables as list.""" assert ( template.render_complex( @@ -1255,44 +1272,50 @@ def test_passing_vars_as_dict_element(hass): ) -def test_passing_vars_as_dict(hass): +def test_passing_vars_as_dict(hass: HomeAssistant) -> None: """Test passing variables as list.""" assert template.render_complex( template.Template("{{ hello }}", hass), {"hello": {"foo": "bar"}} ) == {"foo": "bar"} -def test_render_with_possible_json_value_with_valid_json(hass): +def test_render_with_possible_json_value_with_valid_json(hass: HomeAssistant) -> None: """Render with possible JSON value with valid JSON.""" tpl = template.Template("{{ value_json.hello }}", hass) assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "world" -def test_render_with_possible_json_value_with_invalid_json(hass): +def test_render_with_possible_json_value_with_invalid_json(hass: HomeAssistant) -> None: """Render with possible JSON value with invalid JSON.""" tpl = template.Template("{{ value_json }}", hass) assert tpl.async_render_with_possible_json_value("{ I AM NOT JSON }") == "" -def test_render_with_possible_json_value_with_template_error_value(hass): +def test_render_with_possible_json_value_with_template_error_value( + hass: HomeAssistant, +) -> None: """Render with possible JSON value with template error value.""" tpl = template.Template("{{ non_existing.variable }}", hass) assert tpl.async_render_with_possible_json_value("hello", "-") == "-" -def test_render_with_possible_json_value_with_missing_json_value(hass): +def test_render_with_possible_json_value_with_missing_json_value( + hass: HomeAssistant, +) -> None: """Render with possible JSON value with unknown JSON object.""" tpl = template.Template("{{ value_json.goodbye }}", hass) assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "" -def test_render_with_possible_json_value_valid_with_is_defined(hass): +def test_render_with_possible_json_value_valid_with_is_defined( + hass: HomeAssistant, +) -> None: """Render with possible JSON value with known JSON object.""" tpl = template.Template("{{ value_json.hello|is_defined }}", hass) assert tpl.async_render_with_possible_json_value('{"hello": "world"}') == "world" -def test_render_with_possible_json_value_undefined_json(hass): +def test_render_with_possible_json_value_undefined_json(hass: HomeAssistant) -> None: """Render with possible JSON value with unknown JSON object.""" tpl = template.Template("{{ value_json.bye|is_defined }}", hass) assert ( @@ -1301,13 +1324,15 @@ def test_render_with_possible_json_value_undefined_json(hass): ) -def test_render_with_possible_json_value_undefined_json_error_value(hass): +def test_render_with_possible_json_value_undefined_json_error_value( + hass: HomeAssistant, +) -> None: """Render with possible JSON value with unknown JSON object.""" tpl = template.Template("{{ value_json.bye|is_defined }}", hass) assert tpl.async_render_with_possible_json_value('{"hello": "world"}', "") == "" -def test_render_with_possible_json_value_non_string_value(hass): +def test_render_with_possible_json_value_non_string_value(hass: HomeAssistant) -> None: """Render with possible JSON value with non-string value.""" tpl = template.Template( """ @@ -1320,7 +1345,7 @@ def test_render_with_possible_json_value_non_string_value(hass): assert tpl.async_render_with_possible_json_value(value) == expected -def test_if_state_exists(hass): +def test_if_state_exists(hass: HomeAssistant) -> None: """Test if state exists works.""" hass.states.async_set("test.object", "available") tpl = template.Template( @@ -1329,7 +1354,7 @@ def test_if_state_exists(hass): assert tpl.async_render() == "exists" -def test_is_state(hass): +def test_is_state(hass: HomeAssistant) -> None: """Test is_state method.""" hass.states.async_set("test.object", "available") tpl = template.Template( @@ -1364,8 +1389,16 @@ def test_is_state(hass): ) assert tpl.async_render() == "test.object" + tpl = template.Template( + """ +{{ is_state("test.object", ["on", "off", "available"]) }} + """, + hass, + ) + assert tpl.async_render() is True -def test_is_state_attr(hass): + +def test_is_state_attr(hass: HomeAssistant) -> None: """Test is_state_attr method.""" hass.states.async_set("test.object", "available", {"mode": "on"}) tpl = template.Template( @@ -1401,7 +1434,7 @@ def test_is_state_attr(hass): assert tpl.async_render() == "test.object" -def test_state_attr(hass): +def test_state_attr(hass: HomeAssistant) -> None: """Test state_attr method.""" hass.states.async_set( "test.object", "available", {"effect": "action", "mode": "on"} @@ -1439,7 +1472,7 @@ def test_state_attr(hass): assert tpl.async_render() == "action" -def test_states_function(hass): +def test_states_function(hass: HomeAssistant) -> None: """Test using states as a function.""" hass.states.async_set("test.object", "available") tpl = template.Template('{{ states("test.object") }}', hass) @@ -1648,7 +1681,7 @@ def test_timedelta(mock_is_safe, hass): assert result == "15 days" -def test_version(hass): +def test_version(hass: HomeAssistant) -> None: """Test version filter and function.""" filter_result = template.Template( "{{ '2099.9.9' | version}}", @@ -1687,7 +1720,7 @@ def test_version(hass): ).async_render() -def test_regex_match(hass): +def test_regex_match(hass: HomeAssistant) -> None: """Test regex_match method.""" tpl = template.Template( r""" @@ -1722,7 +1755,7 @@ def test_regex_match(hass): assert tpl.async_render() is True -def test_match_test(hass): +def test_match_test(hass: HomeAssistant) -> None: """Test match test.""" tpl = template.Template( r""" @@ -1733,7 +1766,7 @@ def test_match_test(hass): assert tpl.async_render() is True -def test_regex_search(hass): +def test_regex_search(hass: HomeAssistant) -> None: """Test regex_search method.""" tpl = template.Template( r""" @@ -1768,7 +1801,7 @@ def test_regex_search(hass): assert tpl.async_render() is True -def test_search_test(hass): +def test_search_test(hass: HomeAssistant) -> None: """Test search test.""" tpl = template.Template( r""" @@ -1779,7 +1812,7 @@ def test_search_test(hass): assert tpl.async_render() is True -def test_regex_replace(hass): +def test_regex_replace(hass: HomeAssistant) -> None: """Test regex_replace method.""" tpl = template.Template( r""" @@ -1798,7 +1831,7 @@ def test_regex_replace(hass): assert tpl.async_render() == ["Home Assistant test"] -def test_regex_findall(hass): +def test_regex_findall(hass: HomeAssistant) -> None: """Test regex_findall method.""" tpl = template.Template( """ @@ -1809,7 +1842,7 @@ def test_regex_findall(hass): assert tpl.async_render() == ["JFK", "LHR"] -def test_regex_findall_index(hass): +def test_regex_findall_index(hass: HomeAssistant) -> None: """Test regex_findall_index method.""" tpl = template.Template( """ @@ -1836,7 +1869,7 @@ def test_regex_findall_index(hass): assert tpl.async_render() == "LHR" -def test_bitwise_and(hass): +def test_bitwise_and(hass: HomeAssistant) -> None: """Test bitwise_and method.""" tpl = template.Template( """ @@ -1861,7 +1894,7 @@ def test_bitwise_and(hass): assert tpl.async_render() == 8 & 2 -def test_bitwise_or(hass): +def test_bitwise_or(hass: HomeAssistant) -> None: """Test bitwise_or method.""" tpl = template.Template( """ @@ -2020,7 +2053,7 @@ def test_unpack(hass, caplog): ) -def test_distance_function_with_1_state(hass): +def test_distance_function_with_1_state(hass: HomeAssistant) -> None: """Test distance function with 1 state.""" _set_up_units(hass) hass.states.async_set( @@ -2030,7 +2063,7 @@ def test_distance_function_with_1_state(hass): assert tpl.async_render() == 187 -def test_distance_function_with_2_states(hass): +def test_distance_function_with_2_states(hass: HomeAssistant) -> None: """Test distance function with 2 states.""" _set_up_units(hass) hass.states.async_set( @@ -2047,14 +2080,14 @@ def test_distance_function_with_2_states(hass): assert tpl.async_render() == 187 -def test_distance_function_with_1_coord(hass): +def test_distance_function_with_1_coord(hass: HomeAssistant) -> None: """Test distance function with 1 coord.""" _set_up_units(hass) tpl = template.Template('{{ distance("32.87336", "-117.22943") | round }}', hass) assert tpl.async_render() == 187 -def test_distance_function_with_2_coords(hass): +def test_distance_function_with_2_coords(hass: HomeAssistant) -> None: """Test distance function with 2 coords.""" _set_up_units(hass) assert ( @@ -2067,7 +2100,7 @@ def test_distance_function_with_2_coords(hass): ) -def test_distance_function_with_1_state_1_coord(hass): +def test_distance_function_with_1_state_1_coord(hass: HomeAssistant) -> None: """Test distance function with 1 state 1 coord.""" _set_up_units(hass) hass.states.async_set( @@ -2088,7 +2121,7 @@ def test_distance_function_with_1_state_1_coord(hass): assert tpl2.async_render() == 187 -def test_distance_function_return_none_if_invalid_state(hass): +def test_distance_function_return_none_if_invalid_state(hass: HomeAssistant) -> None: """Test distance function return None if invalid state.""" hass.states.async_set("test.object_2", "happy", {"latitude": 10}) tpl = template.Template("{{ distance(states.test.object_2) | round }}", hass) @@ -2096,7 +2129,7 @@ def test_distance_function_return_none_if_invalid_state(hass): tpl.async_render() -def test_distance_function_return_none_if_invalid_coord(hass): +def test_distance_function_return_none_if_invalid_coord(hass: HomeAssistant) -> None: """Test distance function return None if invalid coord.""" assert ( template.Template('{{ distance("123", "abc") }}', hass).async_render() is None @@ -2113,7 +2146,7 @@ def test_distance_function_return_none_if_invalid_coord(hass): assert tpl.async_render() is None -def test_distance_function_with_2_entity_ids(hass): +def test_distance_function_with_2_entity_ids(hass: HomeAssistant) -> None: """Test distance function with 2 entity ids.""" _set_up_units(hass) hass.states.async_set( @@ -2130,7 +2163,7 @@ def test_distance_function_with_2_entity_ids(hass): assert tpl.async_render() == 187 -def test_distance_function_with_1_entity_1_coord(hass): +def test_distance_function_with_1_entity_1_coord(hass: HomeAssistant) -> None: """Test distance function with 1 entity_id and 1 coord.""" _set_up_units(hass) hass.states.async_set( @@ -2144,7 +2177,7 @@ def test_distance_function_with_1_entity_1_coord(hass): assert tpl.async_render() == 187 -def test_closest_function_home_vs_domain(hass): +def test_closest_function_home_vs_domain(hass: HomeAssistant) -> None: """Test closest function home vs domain.""" hass.states.async_set( "test_domain.object", @@ -2176,7 +2209,7 @@ def test_closest_function_home_vs_domain(hass): ) -def test_closest_function_home_vs_all_states(hass): +def test_closest_function_home_vs_all_states(hass: HomeAssistant) -> None: """Test closest function home vs all states.""" hass.states.async_set( "test_domain.object", @@ -2204,7 +2237,7 @@ def test_closest_function_home_vs_all_states(hass): ) -async def test_closest_function_home_vs_group_entity_id(hass): +async def test_closest_function_home_vs_group_entity_id(hass: HomeAssistant) -> None: """Test closest function home vs group entity id.""" hass.states.async_set( "test_domain.object", @@ -2232,7 +2265,7 @@ async def test_closest_function_home_vs_group_entity_id(hass): assert info.rate_limit is None -async def test_closest_function_home_vs_group_state(hass): +async def test_closest_function_home_vs_group_state(hass: HomeAssistant) -> None: """Test closest function home vs group state.""" hass.states.async_set( "test_domain.object", @@ -2266,7 +2299,7 @@ async def test_closest_function_home_vs_group_state(hass): assert info.rate_limit is None -async def test_expand(hass): +async def test_expand(hass: HomeAssistant) -> None: """Test expand function.""" info = render_to_info(hass, "{{ expand('test.object') }}") assert_result_info(info, [], ["test.object"]) @@ -2429,7 +2462,7 @@ async def test_expand(hass): ) -async def test_device_entities(hass): +async def test_device_entities(hass: HomeAssistant) -> None: """Test device_entities function.""" config_entry = MockConfigEntry(domain="light") device_registry = mock_device_registry(hass) @@ -2502,7 +2535,7 @@ async def test_device_entities(hass): assert info.rate_limit is None -async def test_integration_entities(hass): +async def test_integration_entities(hass: HomeAssistant) -> None: """Test integration_entities function.""" entity_registry = mock_registry(hass) @@ -2540,7 +2573,7 @@ async def test_integration_entities(hass): assert info.rate_limit is None -async def test_config_entry_id(hass): +async def test_config_entry_id(hass: HomeAssistant) -> None: """Test config_entry_id function.""" config_entry = MockConfigEntry(domain="light", title="Some integration") config_entry.add_to_hass(hass) @@ -2566,7 +2599,7 @@ async def test_config_entry_id(hass): assert info.rate_limit is None -async def test_device_id(hass): +async def test_device_id(hass: HomeAssistant) -> None: """Test device_id function.""" config_entry = MockConfigEntry(domain="light") device_registry = mock_device_registry(hass) @@ -2609,7 +2642,7 @@ async def test_device_id(hass): assert info.rate_limit is None -async def test_device_attr(hass): +async def test_device_attr(hass: HomeAssistant) -> None: """Test device_attr and is_device_attr functions.""" config_entry = MockConfigEntry(domain="light") device_registry = mock_device_registry(hass) @@ -2713,8 +2746,23 @@ async def test_device_attr(hass): assert_result_info(info, True) assert info.rate_limit is None + # Test filter syntax (device_attr) + info = render_to_info( + hass, f"{{{{ '{entity_entry.entity_id}' | device_attr('model') }}}}" + ) + assert_result_info(info, "test") + assert info.rate_limit is None -async def test_area_id(hass): + # Test test syntax (is_device_attr) + info = render_to_info( + hass, + f"{{{{ ['{device_entry.id}'] | select('is_device_attr', 'model', 'test') | list }}}}", + ) + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + +async def test_area_id(hass: HomeAssistant) -> None: """Test area_id function.""" config_entry = MockConfigEntry(domain="light") device_registry = mock_device_registry(hass) @@ -2818,7 +2866,7 @@ async def test_area_id(hass): assert info.rate_limit is None -async def test_area_name(hass): +async def test_area_name(hass: HomeAssistant) -> None: """Test area_name function.""" config_entry = MockConfigEntry(domain="light") device_registry = mock_device_registry(hass) @@ -2897,7 +2945,7 @@ async def test_area_name(hass): assert info.rate_limit is None -async def test_area_entities(hass): +async def test_area_entities(hass: HomeAssistant) -> None: """Test area_entities function.""" config_entry = MockConfigEntry(domain="light") entity_registry = mock_registry(hass) @@ -2950,7 +2998,7 @@ async def test_area_entities(hass): assert info.rate_limit is None -async def test_area_devices(hass): +async def test_area_devices(hass: HomeAssistant) -> None: """Test area_devices function.""" config_entry = MockConfigEntry(domain="light") device_registry = mock_device_registry(hass) @@ -2982,7 +3030,7 @@ async def test_area_devices(hass): assert info.rate_limit is None -def test_closest_function_to_coord(hass): +def test_closest_function_to_coord(hass: HomeAssistant) -> None: """Test closest function to coord.""" hass.states.async_set( "test_domain.closest_home", @@ -3028,7 +3076,7 @@ def test_closest_function_to_coord(hass): assert tpl.async_render() == "test_domain.closest_zone" -def test_async_render_to_info_with_branching(hass): +def test_async_render_to_info_with_branching(hass: HomeAssistant) -> None: """Test async_render_to_info function by domain.""" hass.states.async_set("light.a", "off") hass.states.async_set("light.b", "on") @@ -3060,7 +3108,7 @@ def test_async_render_to_info_with_branching(hass): assert info.rate_limit is None -def test_async_render_to_info_with_complex_branching(hass): +def test_async_render_to_info_with_complex_branching(hass: HomeAssistant) -> None: """Test async_render_to_info function by domain.""" hass.states.async_set("light.a", "off") hass.states.async_set("light.b", "on") @@ -3097,7 +3145,9 @@ def test_async_render_to_info_with_complex_branching(hass): assert info.rate_limit == template.DOMAIN_STATES_RATE_LIMIT -async def test_async_render_to_info_with_wildcard_matching_entity_id(hass): +async def test_async_render_to_info_with_wildcard_matching_entity_id( + hass: HomeAssistant, +) -> None: """Test tracking template with a wildcard.""" template_complex_str = r""" @@ -3119,7 +3169,9 @@ async def test_async_render_to_info_with_wildcard_matching_entity_id(hass): assert info.rate_limit == template.DOMAIN_STATES_RATE_LIMIT -async def test_async_render_to_info_with_wildcard_matching_state(hass): +async def test_async_render_to_info_with_wildcard_matching_state( + hass: HomeAssistant, +) -> None: """Test tracking template with a wildcard.""" template_complex_str = """ @@ -3170,7 +3222,7 @@ async def test_async_render_to_info_with_wildcard_matching_state(hass): assert info.rate_limit == template.DOMAIN_STATES_RATE_LIMIT -def test_nested_async_render_to_info_case(hass): +def test_nested_async_render_to_info_case(hass: HomeAssistant) -> None: """Test a deeply nested state with async_render_to_info.""" hass.states.async_set("input_select.picker", "vacuum.a") @@ -3183,7 +3235,7 @@ def test_nested_async_render_to_info_case(hass): assert info.rate_limit is None -def test_result_as_boolean(hass): +def test_result_as_boolean(hass: HomeAssistant) -> None: """Test converting a template result to a boolean.""" assert template.result_as_boolean(True) is True @@ -3213,7 +3265,7 @@ def test_result_as_boolean(hass): assert template.result_as_boolean(None) is False -def test_closest_function_to_entity_id(hass): +def test_closest_function_to_entity_id(hass: HomeAssistant) -> None: """Test closest function to entity id.""" hass.states.async_set( "test_domain.closest_home", @@ -3270,7 +3322,7 @@ def test_closest_function_to_entity_id(hass): ) -def test_closest_function_to_state(hass): +def test_closest_function_to_state(hass: HomeAssistant) -> None: """Test closest function to state.""" hass.states.async_set( "test_domain.closest_home", @@ -3307,7 +3359,7 @@ def test_closest_function_to_state(hass): ) -def test_closest_function_invalid_state(hass): +def test_closest_function_invalid_state(hass: HomeAssistant) -> None: """Test closest function invalid state.""" hass.states.async_set( "test_domain.closest_home", @@ -3325,7 +3377,7 @@ def test_closest_function_invalid_state(hass): ) -def test_closest_function_state_with_invalid_location(hass): +def test_closest_function_state_with_invalid_location(hass: HomeAssistant) -> None: """Test closest function state with invalid location.""" hass.states.async_set( "test_domain.closest_home", @@ -3341,7 +3393,7 @@ def test_closest_function_state_with_invalid_location(hass): ) -def test_closest_function_invalid_coordinates(hass): +def test_closest_function_invalid_coordinates(hass: HomeAssistant) -> None: """Test closest function invalid coordinates.""" hass.states.async_set( "test_domain.closest_home", @@ -3366,14 +3418,14 @@ def test_closest_function_invalid_coordinates(hass): ) -def test_closest_function_no_location_states(hass): +def test_closest_function_no_location_states(hass: HomeAssistant) -> None: """Test closest function without location states.""" assert ( template.Template("{{ closest(states).entity_id }}", hass).async_render() == "" ) -def test_generate_filter_iterators(hass): +def test_generate_filter_iterators(hass: HomeAssistant) -> None: """Test extract entities function with none entities stuff.""" info = render_to_info( hass, @@ -3430,7 +3482,7 @@ def test_generate_filter_iterators(hass): assert_result_info(info, "sensor.test_sensor=value,", [], ["sensor"]) -def test_generate_select(hass): +def test_generate_select(hass: HomeAssistant) -> None: """Test extract entities function with none entities stuff.""" template_str = """ {{ states.sensor|selectattr("state","equalto","off") @@ -3455,7 +3507,7 @@ def test_generate_select(hass): assert info.domains_lifecycle == {"sensor"} -async def test_async_render_to_info_in_conditional(hass): +async def test_async_render_to_info_in_conditional(hass: HomeAssistant) -> None: """Test extract entities function with none entities stuff.""" template_str = """ {{ states("sensor.xyz") == "dog" }} @@ -3491,7 +3543,7 @@ async def test_async_render_to_info_in_conditional(hass): assert_result_info(info, "oink", ["sensor.xyz", "sensor.pig"], []) -def test_jinja_namespace(hass): +def test_jinja_namespace(hass: HomeAssistant) -> None: """Test Jinja's namespace command can be used.""" test_template = template.Template( ( @@ -3509,7 +3561,7 @@ def test_jinja_namespace(hass): assert test_template.async_render() == "another value" -def test_state_with_unit(hass): +def test_state_with_unit(hass: HomeAssistant) -> None: """Test the state_with_unit property helper.""" hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) hass.states.async_set("sensor.test2", "wow") @@ -3533,7 +3585,7 @@ def test_state_with_unit(hass): assert tpl.async_render() == "" -def test_length_of_states(hass): +def test_length_of_states(hass: HomeAssistant) -> None: """Test fetching the length of states.""" hass.states.async_set("sensor.test", "23") hass.states.async_set("sensor.test2", "wow") @@ -3546,14 +3598,14 @@ def test_length_of_states(hass): assert tpl.async_render() == 2 -def test_render_complex_handling_non_template_values(hass): +def test_render_complex_handling_non_template_values(hass: HomeAssistant) -> None: """Test that we can render non-template fields.""" assert template.render_complex( {True: 1, False: template.Template("{{ hello }}", hass)}, {"hello": 2} ) == {True: 1, False: 2} -def test_urlencode(hass): +def test_urlencode(hass: HomeAssistant) -> None: """Test the urlencode method.""" tpl = template.Template( ("{% set dict = {'foo': 'x&y', 'bar': 42} %}{{ dict | urlencode }}"), @@ -3611,7 +3663,7 @@ def test_iif(hass: HomeAssistant) -> None: assert tpl.async_render() == "no" -async def test_cache_garbage_collection(): +async def test_cache_garbage_collection() -> None: """Test caching a template.""" template_string = ( "{% set dict = {'foo': 'x&y', 'bar': 42} %} {{ dict | urlencode }}" @@ -3642,7 +3694,7 @@ async def test_cache_garbage_collection(): ) # pylint: disable=protected-access -def test_is_template_string(): +def test_is_template_string() -> None: """Test is template string.""" assert template.is_template_string("{{ x }}") is True assert template.is_template_string("{% if x == 2 %}1{% else %}0{%end if %}") is True @@ -3651,7 +3703,7 @@ def test_is_template_string(): assert template.is_template_string("Some Text") is False -async def test_protected_blocked(hass): +async def test_protected_blocked(hass: HomeAssistant) -> None: """Test accessing __getattr__ produces a template error.""" tmp = template.Template('{{ states.__getattr__("any") }}', hass) with pytest.raises(TemplateError): @@ -3666,7 +3718,7 @@ async def test_protected_blocked(hass): tmp.async_render() -async def test_demo_template(hass): +async def test_demo_template(hass: HomeAssistant) -> None: """Test the demo template works as expected.""" hass.states.async_set( "sun.sun", @@ -3709,7 +3761,7 @@ For loop example getting 3 entity values: assert "sun" in result -async def test_slice_states(hass): +async def test_slice_states(hass: HomeAssistant) -> None: """Test iterating states with a slice.""" hass.states.async_set("sensor.test", "23") @@ -3720,7 +3772,7 @@ async def test_slice_states(hass): assert tpl.async_render() == "sensor.test" -async def test_lifecycle(hass): +async def test_lifecycle(hass: HomeAssistant) -> None: """Test that we limit template render info for lifecycle events.""" hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"}) for i in range(2): @@ -3756,7 +3808,7 @@ async def test_lifecycle(hass): assert info.filter_lifecycle("sensor.removed") is True -async def test_template_timeout(hass): +async def test_template_timeout(hass: HomeAssistant) -> None: """Test to see if a template will timeout.""" for i in range(2): hass.states.async_set(f"sensor.sensor{i}", "on") @@ -3781,14 +3833,14 @@ async def test_template_timeout(hass): assert await tmp5.async_render_will_timeout(0.000001) is True -async def test_template_timeout_raise(hass): +async def test_template_timeout_raise(hass: HomeAssistant) -> None: """Test we can raise from.""" tmp2 = template.Template("{{ error_invalid + 1 }}", hass) with pytest.raises(TemplateError): assert await tmp2.async_render_will_timeout(3) is False -async def test_lights(hass): +async def test_lights(hass: HomeAssistant) -> None: """Test we can sort lights.""" tmpl = """ @@ -3818,7 +3870,7 @@ async def test_lights(hass): assert f"sensor{i}" in info.result() -async def test_template_errors(hass): +async def test_template_errors(hass: HomeAssistant) -> None: """Test template rendering wraps exceptions with TemplateError.""" with pytest.raises(TemplateError): @@ -3834,7 +3886,7 @@ async def test_template_errors(hass): template.Template("{{ utcnow() | random }}", hass).async_render() -async def test_state_attributes(hass): +async def test_state_attributes(hass: HomeAssistant) -> None: """Test state attributes.""" hass.states.async_set("sensor.test", "23") @@ -3882,7 +3934,7 @@ async def test_state_attributes(hass): tpl.async_render() -async def test_unavailable_states(hass): +async def test_unavailable_states(hass: HomeAssistant) -> None: """Test watching unavailable states.""" for i in range(10): @@ -3905,7 +3957,7 @@ async def test_unavailable_states(hass): assert tpl.async_render() == "light.none, light.unavailable, light.unknown" -async def test_legacy_templates(hass): +async def test_legacy_templates(hass: HomeAssistant) -> None: """Test if old template behavior works when legacy templates are enabled.""" hass.states.async_set("sensor.temperature", "12") @@ -3921,7 +3973,7 @@ async def test_legacy_templates(hass): ) -async def test_no_result_parsing(hass): +async def test_no_result_parsing(hass: HomeAssistant) -> None: """Test if templates results are not parsed.""" hass.states.async_set("sensor.temperature", "12") @@ -3943,14 +3995,14 @@ async def test_no_result_parsing(hass): ) -async def test_is_static_still_ast_evals(hass): +async def test_is_static_still_ast_evals(hass: HomeAssistant) -> None: """Test is_static still converts to native type.""" tpl = template.Template("[1, 2]", hass) assert tpl.is_static assert tpl.async_render() == [1, 2] -async def test_result_wrappers(hass): +async def test_result_wrappers(hass: HomeAssistant) -> None: """Test result wrappers.""" for text, native, orig_type, schema in ( ("[1, 2]", [1, 2], list, vol.Schema([int])), @@ -3973,7 +4025,7 @@ async def test_result_wrappers(hass): ) -async def test_parse_result(hass): +async def test_parse_result(hass: HomeAssistant) -> None: """Test parse result.""" for tpl, result in ( ('{{ "{{}}" }}', "{{}}"), @@ -4010,7 +4062,7 @@ async def test_undefined_variable(hass, caplog): ) -async def test_template_states_blocks_setitem(hass): +async def test_template_states_blocks_setitem(hass: HomeAssistant) -> None: """Test we cannot setitem on TemplateStates.""" hass.states.async_set("light.new", STATE_ON) state = hass.states.get("light.new") @@ -4019,7 +4071,7 @@ async def test_template_states_blocks_setitem(hass): template_state["any"] = "any" -async def test_template_states_can_serialize(hass): +async def test_template_states_can_serialize(hass: HomeAssistant) -> None: """Test TemplateState is serializable.""" hass.states.async_set("light.new", STATE_ON) state = hass.states.get("light.new") diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 9cd3b0956ce..4718e3130d5 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -1,11 +1,13 @@ """The tests for the trigger helper.""" -from unittest.mock import ANY, MagicMock, call, patch +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch import pytest import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import Context, HomeAssistant, ServiceCall, callback from homeassistant.helpers.trigger import ( + DATA_PLUGGABLE_ACTIONS, + PluggableAction, _async_get_trigger_platform, async_initialize_triggers, async_validate_trigger_config, @@ -197,3 +199,80 @@ async def test_async_initialize_triggers( log_cb.reset_mock() unsub() + + +async def test_pluggable_action(hass: HomeAssistant, calls: list[ServiceCall]): + """Test normal behavior of pluggable actions.""" + update_1 = MagicMock() + update_2 = MagicMock() + action_1 = AsyncMock() + action_2 = AsyncMock() + trigger_1 = {"domain": "test", "device": "1"} + trigger_2 = {"domain": "test", "device": "2"} + variables_1 = {"source": "test 1"} + variables_2 = {"source": "test 2"} + context_1 = Context() + context_2 = Context() + + plug_1 = PluggableAction(update_1) + plug_2 = PluggableAction(update_2) + + # Verify plug is inactive without triggers + remove_plug_1 = plug_1.async_register(hass, trigger_1) + assert not plug_1 + assert not plug_2 + + # Verify plug remain inactive with non matching trigger + remove_attach_2 = PluggableAction.async_attach_trigger( + hass, trigger_2, action_2, variables_2 + ) + assert not plug_1 + assert not plug_2 + update_1.assert_not_called() + update_2.assert_not_called() + + # Verify plug is active, and update when matching trigger attaches + remove_attach_1 = PluggableAction.async_attach_trigger( + hass, trigger_1, action_1, variables_1 + ) + assert plug_1 + assert not plug_2 + update_1.assert_called() + update_1.reset_mock() + update_2.assert_not_called() + + # Verify a non registered plug is inactive + remove_plug_1() + assert not plug_1 + assert not plug_2 + + # Verify a plug registered to existing trigger is true + remove_plug_1 = plug_1.async_register(hass, trigger_1) + assert plug_1 + assert not plug_2 + + remove_plug_2 = plug_2.async_register(hass, trigger_2) + assert plug_1 + assert plug_2 + + # Verify no actions should have been triggered so far + action_1.assert_not_called() + action_2.assert_not_called() + + # Verify action is triggered with correct data + await plug_1.async_run(hass, context_1) + await plug_2.async_run(hass, context_2) + action_1.assert_called_with(variables_1, context_1) + action_2.assert_called_with(variables_2, context_2) + + # Verify plug goes inactive if trigger is removed + remove_attach_1() + assert not plug_1 + + # Verify registry is cleaned when no plugs nor triggers are attached + assert hass.data[DATA_PLUGGABLE_ACTIONS] + remove_plug_1() + remove_plug_2() + remove_attach_2() + assert not hass.data[DATA_PLUGGABLE_ACTIONS] + assert not plug_2 diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 3ab19450879..a5478d149ec 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -108,7 +108,7 @@ async def test_change_password_invalid_user(hass, provider, capsys, hass_storage data.validate_login("invalid-user", "new-pass") -def test_parsing_args(loop): +def test_parsing_args(event_loop): """Test we parse args correctly.""" called = False diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 05ebb8fb0e5..fc35b28f7c9 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -43,7 +43,7 @@ def normalize_yaml_files(check_dict): return [key.replace(root, "...") for key in sorted(check_dict["yaml_files"].keys())] -def test_bad_core_config(mock_is_file, loop): +def test_bad_core_config(mock_is_file, event_loop): """Test a bad core config setup.""" files = {YAML_CONFIG_FILE: BAD_CORE_CONFIG} with patch_yaml_files(files): @@ -52,7 +52,7 @@ def test_bad_core_config(mock_is_file, loop): assert res["except"]["homeassistant"][1] == {"unit_system": "bad"} -def test_config_platform_valid(mock_is_file, loop): +def test_config_platform_valid(mock_is_file, event_loop): """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"} with patch_yaml_files(files): @@ -65,7 +65,7 @@ def test_config_platform_valid(mock_is_file, loop): assert len(res["yaml_files"]) == 1 -def test_component_platform_not_found(mock_is_file, loop): +def test_component_platform_not_found(mock_is_file, event_loop): """Test errors if component or platform not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"} @@ -96,7 +96,7 @@ def test_component_platform_not_found(mock_is_file, loop): assert len(res["yaml_files"]) == 1 -def test_secrets(mock_is_file, loop): +def test_secrets(mock_is_file, event_loop): """Test secrets config checking method.""" secrets_path = get_test_config_dir("secrets.yaml") @@ -127,7 +127,7 @@ def test_secrets(mock_is_file, loop): ] -def test_package_invalid(mock_is_file, loop): +def test_package_invalid(mock_is_file, event_loop): """Test an invalid package.""" files = { YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') @@ -145,7 +145,7 @@ def test_package_invalid(mock_is_file, loop): assert len(res["yaml_files"]) == 1 -def test_bootstrap_error(loop): +def test_bootstrap_error(event_loop): """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "automation: !include no.yaml"} with patch_yaml_files(files): diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index e51f4d315ee..c8365c86a9a 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -460,7 +460,7 @@ async def test_setup_hass( mock_ensure_config_exists, mock_process_ha_config_upgrade, caplog, - loop, + event_loop, ): """Test it works.""" verbose = Mock() @@ -511,7 +511,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( mock_ensure_config_exists, mock_process_ha_config_upgrade, caplog, - loop, + event_loop, ): """Test it works.""" verbose = Mock() @@ -553,7 +553,7 @@ async def test_setup_hass_invalid_yaml( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" with patch( @@ -581,7 +581,7 @@ async def test_setup_hass_config_dir_nonexistent( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" mock_ensure_config_exists.return_value = False @@ -608,7 +608,7 @@ async def test_setup_hass_safe_mode( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" with patch("homeassistant.components.browser.setup") as browser_setup, patch( @@ -641,7 +641,7 @@ async def test_setup_hass_invalid_core_config( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test it works.""" with patch( @@ -669,7 +669,7 @@ async def test_setup_safe_mode_if_no_frontend( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, - loop, + event_loop, ): """Test we setup safe mode if frontend didn't load.""" verbose = Mock() diff --git a/tests/test_config.py b/tests/test_config.py index 0a125d8f121..ea9c81eae1a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -28,7 +28,7 @@ from homeassistant.const import ( __version__, ) from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, issue_registry as ir import homeassistant.helpers.check_config as check_config from homeassistant.helpers.entity import Entity from homeassistant.loader import async_get_integration @@ -40,7 +40,7 @@ from homeassistant.util.unit_system import ( ) from homeassistant.util.yaml import SECRET_YAML -from tests.common import get_test_config_dir, patch_yaml_files +from tests.common import MockUser, get_test_config_dir, patch_yaml_files CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -214,6 +214,8 @@ def test_core_config_schema(): {"customize": "bla"}, {"customize": {"light.sensor": 100}}, {"customize": {"entity_id": []}}, + {"country": "xx"}, + {"language": "xx"}, ): with pytest.raises(MultipleInvalid): config_util.CORE_CONFIG_SCHEMA(value) @@ -228,6 +230,8 @@ def test_core_config_schema(): CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, "currency": "USD", "customize": {"sensor.temperature": {"hidden": True}}, + "country": "SE", + "language": "sv", } ) @@ -393,9 +397,12 @@ async def test_loading_configuration_from_storage(hass, hass_storage): "external_url": "https://www.example.com", "internal_url": "http://example.local", "currency": "EUR", + "country": "SE", + "language": "sv", }, "key": "core.config", "version": 1, + "minor_version": 3, } await config_util.async_process_ha_core_config( hass, {"allowlist_external_dirs": "/etc"} @@ -410,6 +417,8 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert hass.config.currency == "EUR" + assert hass.config.country == "SE" + assert hass.config.language == "sv" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source is ConfigSource.STORAGE @@ -445,7 +454,7 @@ async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_stor assert hass.config.config_source is ConfigSource.STORAGE -async def test_igration_and_updating_configuration(hass, hass_storage): +async def test_migration_and_updating_configuration(hass, hass_storage): """Test updating configuration stores the new configuration.""" core_data = { "data": { @@ -475,10 +484,15 @@ async def test_igration_and_updating_configuration(hass, hass_storage): expected_new_core_data["data"]["currency"] = "USD" # 1.1 -> 1.2 store migration with migrated unit system expected_new_core_data["data"]["unit_system_v2"] = "us_customary" - expected_new_core_data["minor_version"] = 2 + expected_new_core_data["minor_version"] = 3 + # defaults for country and language + expected_new_core_data["data"]["country"] = None + expected_new_core_data["data"]["language"] = "en" assert hass_storage["core.config"] == expected_new_core_data assert hass.config.latitude == 50 assert hass.config.currency == "USD" + assert hass.config.country is None + assert hass.config.language == "en" async def test_override_stored_configuration(hass, hass_storage): @@ -527,6 +541,8 @@ async def test_loading_configuration(hass): "media_dirs": {"mymedia": "/usr"}, "legacy_templates": True, "currency": "EUR", + "country": "SE", + "language": "sv", }, ) @@ -545,6 +561,74 @@ async def test_loading_configuration(hass): assert hass.config.config_source is ConfigSource.YAML assert hass.config.legacy_templates is True assert hass.config.currency == "EUR" + assert hass.config.country == "SE" + assert hass.config.language == "sv" + + +@pytest.mark.parametrize( + "minor_version, users, user_data, default_language", + ( + (2, (), {}, "en"), + (2, ({"is_owner": True},), {}, "en"), + ( + 2, + ({"id": "user1", "is_owner": True},), + {"user1": {"language": {"language": "sv"}}}, + "sv", + ), + ( + 2, + ({"id": "user1", "is_owner": False},), + {"user1": {"language": {"language": "sv"}}}, + "en", + ), + (3, (), {}, "en"), + (3, ({"is_owner": True},), {}, "en"), + ( + 3, + ({"id": "user1", "is_owner": True},), + {"user1": {"language": {"language": "sv"}}}, + "en", + ), + ( + 3, + ({"id": "user1", "is_owner": False},), + {"user1": {"language": {"language": "sv"}}}, + "en", + ), + ), +) +async def test_language_default( + hass, hass_storage, minor_version, users, user_data, default_language +): + """Test language config default to owner user's language during migration. + + This should only happen if the core store version < 1.3 + """ + core_data = { + "data": {}, + "key": "core.config", + "version": 1, + "minor_version": minor_version, + } + hass_storage["core.config"] = dict(core_data) + + for user_config in users: + user = MockUser(**user_config).add_to_hass(hass) + if user.id not in user_data: + continue + storage_key = f"frontend.user_data_{user.id}" + hass_storage[storage_key] = { + "key": storage_key, + "version": 1, + "data": user_data[user.id], + } + + await config_util.async_process_ha_core_config( + hass, + {}, + ) + assert hass.config.language == default_language async def test_loading_configuration_default_media_dirs_docker(hass): @@ -1205,3 +1289,67 @@ def test_identify_config_schema(domain, schema, expected): config_util._identify_config_schema(Mock(DOMAIN=domain, CONFIG_SCHEMA=schema)) == expected ) + + +async def test_core_config_schema_historic_currency(hass): + """Test core config schema.""" + await config_util.async_process_ha_core_config(hass, {"currency": "LTT"}) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue("homeassistant", "historic_currency") + assert issue + assert issue.translation_placeholders == {"currency": "LTT"} + + +async def test_core_store_historic_currency(hass, hass_storage): + """Test core config store.""" + core_data = { + "data": { + "currency": "LTT", + }, + "key": "core.config", + "version": 1, + "minor_version": 1, + } + hass_storage["core.config"] = dict(core_data) + await config_util.async_process_ha_core_config(hass, {}) + + issue_registry = ir.async_get(hass) + issue_id = "historic_currency" + issue = issue_registry.async_get_issue("homeassistant", issue_id) + assert issue + assert issue.translation_placeholders == {"currency": "LTT"} + + await hass.config.async_update(**{"currency": "EUR"}) + issue = issue_registry.async_get_issue("homeassistant", issue_id) + assert not issue + + +async def test_core_config_schema_no_country(hass): + """Test core config schema.""" + await config_util.async_process_ha_core_config(hass, {}) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue("homeassistant", "country_not_configured") + assert issue + + +async def test_core_store_no_country(hass, hass_storage): + """Test core config store.""" + core_data = { + "data": {}, + "key": "core.config", + "version": 1, + "minor_version": 1, + } + hass_storage["core.config"] = dict(core_data) + await config_util.async_process_ha_core_config(hass, {}) + + issue_registry = ir.async_get(hass) + issue_id = "country_not_configured" + issue = issue_registry.async_get_issue("homeassistant", issue_id) + assert issue + + await hass.config.async_update(**{"country": "SE"}) + issue = issue_registry.async_get_issue("homeassistant", issue_id) + assert not issue diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 99e26be6d75..b1e3fd760d5 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -20,12 +20,13 @@ from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType from homeassistant.exceptions import ( ConfigEntryAuthFailed, + ConfigEntryError, ConfigEntryNotReady, HomeAssistantError, ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component from homeassistant.util import dt from tests.common import ( @@ -2866,6 +2867,96 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): assert entry.state is config_entries.ConfigEntryState.LOADED +async def test_setup_raise_entry_error(hass, caplog): + """Test a setup raising ConfigEntryError.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock( + side_effect=ConfigEntryError("Incompatible firmware version") + ) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert ( + "Error setting up entry test_title for test: Incompatible firmware version" + in caplog.text + ) + + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert entry.reason == "Incompatible firmware version" + + +async def test_setup_raise_entry_error_from_first_coordinator_update(hass, caplog): + """Test async_config_entry_first_refresh raises ConfigEntryError.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" + + async def _async_update_data(): + raise ConfigEntryError("Incompatible firmware version") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_config_entry_first_refresh() + return True + + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert ( + "Error setting up entry test_title for test: Incompatible firmware version" + in caplog.text + ) + + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert entry.reason == "Incompatible firmware version" + + +async def test_setup_not_raise_entry_error_from_future_coordinator_update(hass, caplog): + """Test a coordinator not raises ConfigEntryError in the future.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" + + async def _async_update_data(): + raise ConfigEntryError("Incompatible firmware version") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_refresh() + return True + + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert ( + "Config entry setup failed while fetching any data: Incompatible firmware version" + in caplog.text + ) + + assert entry.state is config_entries.ConfigEntryState.LOADED + + async def test_setup_raise_auth_failed(hass, caplog): """Test a setup raising ConfigEntryAuthFailed.""" entry = MockConfigEntry(title="test_title", domain="test") @@ -3316,17 +3407,133 @@ async def test_reauth(hass): assert entry.entry_id != entry2.entry_id - # Check we can't start duplicate flows + # Check that we can't start duplicate reauth flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 1 - # Check we can't start duplicate when the context context is different + # Check that we can't start duplicate reauth flows when the context is different entry.async_start_reauth(hass, {"diff": "diff"}) await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 1 - # Check we can start a reauth for a different entry + # Check that we can start a reauth flow for a different entry entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 2 + + +async def test_get_active_flows(hass): + """Test the async_get_active_flows helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init): + entry.async_start_reauth( + hass, + context={"extra_context": "some_extra_context"}, + data={"extra_data": 1234}, + ) + await hass.async_block_till_done() + + # Check that there's an active reauth flow: + active_reauth_flow = next( + iter(entry.async_get_active_flows(hass, {config_entries.SOURCE_REAUTH})), None + ) + assert active_reauth_flow is not None + + # Check that there isn't any other flow (in this case, a user flow): + active_user_flow = next( + iter(entry.async_get_active_flows(hass, {config_entries.SOURCE_USER})), None + ) + assert active_user_flow is None + + +async def test_async_wait_component_dynamic(hass: HomeAssistant): + """Test async_wait_component for a config entry which is dynamically loaded.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + entry.add_to_hass(hass) + + # The config entry is not loaded, and is also not scheduled to load + assert await hass.config_entries.async_wait_component(entry) is False + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # The config entry is loaded + assert await hass.config_entries.async_wait_component(entry) is True + + +async def test_async_wait_component_startup(hass: HomeAssistant): + """Test async_wait_component for a config entry which is loaded at startup.""" + entry = MockConfigEntry(title="test_title", domain="test") + + setup_stall = asyncio.Event() + setup_started = asyncio.Event() + + async def mock_setup(hass: HomeAssistant, _) -> bool: + setup_started.set() + await setup_stall.wait() + return True + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration( + hass, + MockModule("test", async_setup=mock_setup, async_setup_entry=mock_setup_entry), + ) + mock_entity_platform(hass, "config_flow.test", None) + + entry.add_to_hass(hass) + + # The config entry is not loaded, and is also not scheduled to load + assert await hass.config_entries.async_wait_component(entry) is False + + # Mark the component as scheduled to be loaded + async_set_domains_to_be_loaded(hass, {"test"}) + + # Start loading the component, including its config entries + hass.async_create_task(async_setup_component(hass, "test", {})) + await setup_started.wait() + + # The component is not yet loaded + assert "test" not in hass.config.components + + # Allow setup to proceed + setup_stall.set() + + # The component is scheduled to load, this will block until the config entry is loaded + assert await hass.config_entries.async_wait_component(entry) is True + + # The component has been loaded + assert "test" in hass.config.components + + +async def test_options_flow_options_not_mutated() -> None: + """Test that OptionsFlowWithConfigEntry doesn't mutate entry options.""" + entry = MockConfigEntry( + domain="test", + data={"first": True}, + options={"sub_dict": {"1": "one"}, "sub_list": ["one"]}, + ) + + options_flow = config_entries.OptionsFlowWithConfigEntry(entry) + + options_flow._options["sub_dict"]["2"] = "two" + options_flow._options["sub_list"].append("two") + + assert options_flow._options == { + "sub_dict": {"1": "one", "2": "two"}, + "sub_list": ["one", "two"], + } + assert entry.options == {"sub_dict": {"1": "one"}, "sub_list": ["one"]} diff --git a/tests/test_core.py b/tests/test_core.py index 017c8b3b607..2f8db7fc0d6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -87,9 +87,9 @@ def test_async_add_hass_job_schedule_partial_callback(): assert len(hass.add_job.mock_calls) == 0 -def test_async_add_hass_job_schedule_coroutinefunction(loop): +def test_async_add_hass_job_schedule_coroutinefunction(event_loop): """Test that we schedule coroutines and add jobs to the job pool.""" - hass = MagicMock(loop=MagicMock(wraps=loop)) + hass = MagicMock(loop=MagicMock(wraps=event_loop)) async def job(): pass @@ -100,9 +100,9 @@ def test_async_add_hass_job_schedule_coroutinefunction(loop): assert len(hass.add_job.mock_calls) == 0 -def test_async_add_hass_job_schedule_partial_coroutinefunction(loop): +def test_async_add_hass_job_schedule_partial_coroutinefunction(event_loop): """Test that we schedule partial coros and add jobs to the job pool.""" - hass = MagicMock(loop=MagicMock(wraps=loop)) + hass = MagicMock(loop=MagicMock(wraps=event_loop)) async def job(): pass @@ -128,9 +128,9 @@ def test_async_add_job_add_hass_threaded_job_to_pool(): assert len(hass.loop.run_in_executor.mock_calls) == 1 -def test_async_create_task_schedule_coroutine(loop): +def test_async_create_task_schedule_coroutine(event_loop): """Test that we schedule coroutines and add jobs to the job pool.""" - hass = MagicMock(loop=MagicMock(wraps=loop)) + hass = MagicMock(loop=MagicMock(wraps=event_loop)) async def job(): pass @@ -939,6 +939,7 @@ async def test_config_defaults(): assert config.external_url is None assert config.config_source is ha.ConfigSource.DEFAULT assert config.skip_pip is False + assert config.skip_pip_packages == [] assert config.components == set() assert config.api is None assert config.config_dir is None @@ -948,6 +949,8 @@ async def test_config_defaults(): assert config.safe_mode is False assert config.legacy_templates is False assert config.currency == "EUR" + assert config.country is None + assert config.language == "en" async def test_config_path_with_file(): @@ -989,6 +992,8 @@ async def test_config_as_dict(): "external_url": None, "internal_url": None, "currency": "EUR", + "country": None, + "language": "en", } assert expected == config.as_dict() @@ -1075,7 +1080,7 @@ async def test_bad_timezone_raises_value_error(hass): await hass.config.async_update(time_zone="not_a_timezone") -async def test_start_taking_too_long(loop, caplog): +async def test_start_taking_too_long(event_loop, caplog): """Test when async_start takes too long.""" hass = ha.HomeAssistant() caplog.set_level(logging.WARNING) @@ -1094,7 +1099,7 @@ async def test_start_taking_too_long(loop, caplog): assert hass.state == ha.CoreState.stopped -async def test_track_task_functions(loop): +async def test_track_task_functions(event_loop): """Test function to start/stop track task and initial state.""" hass = ha.HomeAssistant() try: diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 1d60e20a3f0..f0bcd2b5fd6 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -327,7 +327,7 @@ async def test_external_step(hass, manager): "refresh": True, } - # Frontend refreshses the flow + # Frontend refreshes the flow result = await manager.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Hello" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index e353c0dd82d..acd11caece2 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,12 +1,17 @@ """Test to verify that Home Assistant exceptions work.""" +from __future__ import annotations + +import pytest + from homeassistant.exceptions import ( ConditionErrorContainer, ConditionErrorIndex, ConditionErrorMessage, + TemplateError, ) -def test_conditionerror_format(): +def test_conditionerror_format() -> None: """Test ConditionError stringifiers.""" error1 = ConditionErrorMessage("test", "A test error") assert str(error1) == "In 'test' condition: A test error" @@ -43,3 +48,16 @@ In 'box' (item 2 of 2): == """In 'box': In 'test' condition: A test error""" ) + + +@pytest.mark.parametrize( + "arg, expected", + [ + ("message", "message"), + (Exception("message"), "Exception: message"), + ], +) +def test_template_message(arg: str | Exception, expected: str) -> None: + """Ensure we can create TemplateError.""" + template_error = TemplateError(arg) + assert str(template_error) == expected diff --git a/tests/test_main.py b/tests/test_main.py index 5ec6460301f..522515b0d31 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -61,3 +61,27 @@ def test_validate_python(mock_exit): assert mock_exit.called is False mock_exit.reset_mock() + + +@patch("sys.exit") +def test_skip_pip_mutually_exclusive(mock_exit): + """Test --skip-pip and --skip-pip-package are mutually exclusive.""" + + def parse_args(*args): + with patch("sys.argv", ["python"] + list(args)): + return main.get_arguments() + + args = parse_args("--skip-pip") + assert args.skip_pip is True + + args = parse_args("--skip-pip-packages", "foo") + assert args.skip_pip is False + assert args.skip_pip_packages == ["foo"] + + args = parse_args("--skip-pip-packages", "foo-asd,bar-xyz") + assert args.skip_pip is False + assert args.skip_pip_packages == ["foo-asd", "bar-xyz"] + + assert mock_exit.called is False + args = parse_args("--skip-pip", "--skip-pip-packages", "foo") + assert mock_exit.called is True diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 77499707489..6dadeba3797 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,4 +1,5 @@ """Test requirements module.""" +import logging import os from unittest.mock import call, patch @@ -93,6 +94,23 @@ async def test_install_missing_package(hass): assert len(mock_inst.mock_calls) == 3 +async def test_install_skipped_package(hass, caplog): + """Test an install attempt on a dependency that should be skipped.""" + with patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_inst: + hass.config.skip_pip_packages = ["hello"] + with caplog.at_level(logging.WARNING): + await async_process_requirements( + hass, "test_component", ["hello==1.0.0", "not_skipped==1.2.3"] + ) + + assert "Skipping requirement hello==1.0.0" in caplog.text + + assert len(mock_inst.mock_calls) == 1 + assert mock_inst.mock_calls[0].args[0] == "not_skipped==1.2.3" + + async def test_get_integration_with_requirements(hass): """Check getting an integration with loaded requirements.""" hass.config.skip_pip = False @@ -401,7 +419,7 @@ async def test_discovery_requirements_mqtt(hass): ) as mock_process: await async_get_integration_with_requirements(hass, "mqtt_comp") - assert len(mock_process.mock_calls) == 2 # mqtt also depends on http + assert len(mock_process.mock_calls) == 3 # mqtt also depends on http assert mock_process.mock_calls[0][1][1] == mqtt.requirements @@ -418,14 +436,15 @@ async def test_discovery_requirements_ssdp(hass): ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 4 + assert len(mock_process.mock_calls) == 5 assert mock_process.mock_calls[0][1][1] == ssdp.requirements # Ensure zeroconf is a dep for ssdp assert { mock_process.mock_calls[1][1][0], mock_process.mock_calls[2][1][0], mock_process.mock_calls[3][1][0], - } == {"network", "zeroconf", "http"} + mock_process.mock_calls[4][1][0], + } == {"http", "network", "recorder", "zeroconf"} @pytest.mark.parametrize( @@ -447,7 +466,7 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 3 # zeroconf also depends on http + assert len(mock_process.mock_calls) == 4 # zeroconf also depends on http assert mock_process.mock_calls[0][1][1] == zeroconf.requirements diff --git a/tests/test_setup.py b/tests/test_setup.py index 04924344c2b..fc07c68bad2 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -639,7 +639,7 @@ async def test_integration_logs_is_custom(hass, caplog): async def test_async_get_loaded_integrations(hass): - """Test we can enumerate loaded integations.""" + """Test we can enumerate loaded integrations.""" hass.config.components.add("notbase") hass.config.components.add("switch") hass.config.components.add("notbase.switch") diff --git a/tests/testing_config/custom_components/test/text.py b/tests/testing_config/custom_components/test/text.py new file mode 100644 index 00000000000..1430259e2e8 --- /dev/null +++ b/tests/testing_config/custom_components/test/text.py @@ -0,0 +1,86 @@ +""" +Provide a mock text platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.text import RestoreText, TextEntity, TextMode + +from tests.common import MockEntity + +UNIQUE_TEXT = "unique_text" + +ENTITIES = [] + + +class MockTextEntity(MockEntity, TextEntity): + """Mock text class.""" + + @property + def native_max(self): + """Return the native native_max.""" + return self._handle("native_max") + + @property + def native_min(self): + """Return the native native_min.""" + return self._handle("native_min") + + @property + def mode(self): + """Return the mode.""" + return self._handle("mode") + + @property + def pattern(self): + """Return the pattern.""" + return self._handle("pattern") + + @property + def native_value(self): + """Return the native value of this text.""" + return self._handle("native_value") + + def set_native_value(self, value: str) -> None: + """Change the selected option.""" + self._values["native_value"] = value + + +class MockRestoreText(MockTextEntity, RestoreText): + """Mock RestoreText class.""" + + async def async_added_to_hass(self) -> None: + """Restore native_*.""" + await super().async_added_to_hass() + if (last_text_data := await self.async_get_last_text_data()) is None: + return + self._values["native_max"] = last_text_data.native_max + self._values["native_min"] = last_text_data.native_min + self._values["native_value"] = last_text_data.native_value + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockTextEntity( + name="test", + native_min=1, + native_max=5, + mode=TextMode.TEXT, + pattern=r"[A-Za-z0-9]", + unique_id=UNIQUE_TEXT, + native_value="Hello", + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test_embedded/switch.py b/tests/testing_config/custom_components/test_embedded/switch.py index 3abd5b79085..46dac4419a6 100644 --- a/tests/testing_config/custom_components/test_embedded/switch.py +++ b/tests/testing_config/custom_components/test_embedded/switch.py @@ -5,4 +5,3 @@ async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): """Find and return test switches.""" - pass diff --git a/tests/util/test_location.py b/tests/util/test_location.py index d8d86965733..51cd8d4388f 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -32,7 +32,7 @@ async def session(hass): @pytest.fixture -async def raising_session(loop): +async def raising_session(event_loop): """Return an aioclient session that only fails.""" return Mock(get=Mock(side_effect=aiohttp.ClientError)) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index cbc98968177..00000000000 --- a/tox.ini +++ /dev/null @@ -1,48 +0,0 @@ -[tox] -envlist = py39, lint, pylint, typing, cov -skip_missing_interpreters = True -ignore_basepython_conflict = True -isolated_build = True - -[testenv] -basepython = {env:PYTHON3_PATH:python3} -# pip version duplicated in homeassistant/package_constraints.txt -pip_version = pip>=21.0,<22.4 -install_command = python -m pip install --use-deprecated legacy-resolver {opts} {packages} -commands = - {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} - {toxinidir}/script/check_dirty -deps = - -r{toxinidir}/requirements_test_all.txt - -[testenv:cov] -commands = - {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} - {toxinidir}/script/check_dirty -deps = - -r{toxinidir}/requirements_test_all.txt - -[testenv:pylint] -skip_install = True -ignore_errors = True -deps = - -r{toxinidir}/requirements_all.txt - -r{toxinidir}/requirements_test.txt -commands = - pylint {env:PYLINT_ARGS:} {posargs} homeassistant - -[testenv:lint] -deps = - -r{toxinidir}/requirements_test.txt -commands = - python -m script.gen_requirements_all validate - python -m script.hassfest --action validate - pre-commit run codespell {posargs: --all-files} - pre-commit run flake8 {posargs: --all-files} - pre-commit run bandit {posargs: --all-files} - -[testenv:typing] -deps = - -r{toxinidir}/requirements_test_all.txt -commands = - mypy homeassistant